當前位置:
首頁 > 知識 > JVM類載入機制概述:載入時機與載入過程

JVM類載入機制概述:載入時機與載入過程

摘要:

我們知道,一個.Java文件在編譯後會形成相應的一個或多個Class文件,這些Class文件中描述了類的各種信息,並且它們最終都需要被載入到虛擬機中才能被運行和使用。事實上,虛擬機把描述類的數據從Class文件載入到內存,並對數據進行校驗,轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型的過程就是虛擬機的類載入機制。本文概述了JVM載入類的時機和生命周期,並結合典型案例重點介紹了類的初始化過程,揭開了JVM類載入機制的神秘面紗。

友情提示:

JVM類載入機制主要包括兩個問題:類載入的時機與步驟類載入的方式。本文主要闡述了第一個問題。

一個Java對象的創建過程往往包括兩個階段:類初始化階段類實例化階段

注意,本文內容是以HotSpot虛擬機為基準的。


一、類載入機制概述

我們知道,一個.java文件在編譯後會形成相應的一個或多個Class文件(若一個類中含有內部類,則編譯後會產生多個Class文件),但這些Class文件中描述的各種信息,最終都需要載入到虛擬機中之後才能被運行和使用。事實上,虛擬機把描述類的數據從Class文件載入到內存,並對數據進行校驗,轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型的過程就是虛擬機的 類載入機制

與那些在編譯時需要進行連接工作的語言不同,在Java語言裡面,類型的載入和連接都是在程序運行期間完成,這樣會在類載入時稍微增加一些性能開銷,但是卻能為Java應用程序提供高度的靈活性,Java中天生可以動態擴展的語言特性多態就是依賴運行期動態載入和動態鏈接這個特點實現的。例如,如果編寫一個使用介面的應用程序,可以等到運行時再指定其實際的實現。這種組裝應用程序的方式廣泛應用於Java程序之中。

既然這樣,那麼,

  • 虛擬機什麼時候才會載入Class文件並初始化類呢?(類載入和初始化時機)

  • 虛擬機如何載入一個Class文件呢?(Java類載入的方式:類載入器、雙親委派機制)

  • 虛擬機載入一個Class文件要經歷那些具體的步驟呢?(類載入過程/步驟)


第一、三個問題就是本文要闡述的重點。特別地,Java類載入器和雙親委派機制等內容已在博文《深入理解Java類載入器(一):Java類載入原理解析》中說明,此不贅述。


二. 類載入的時機

Java類從被載入到虛擬機內存中開始,到卸載出內存為止,它的整個生命周期包括:載入(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using) 和 卸載(Unloading)七個階段。其中準備、驗證、解析3個部分統稱為連接(Linking),如圖所示:

JVM類載入機制概述:載入時機與載入過程

載入、驗證、準備、初始化和卸載這5個階段的順序是確定的,類的載入過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之後再開始,這是為了支持Java語言的運行時綁定(也稱為動態綁定或晚期綁定)。以下陳述的內容都已HotSpot為基準。特別需要注意的是,類的載入過程必須按照這種順序按部就班地「開始」,而不是按部就班的「進行」或「完成」,因為這些階段通常都是相互交叉地混合式進行的,也就是說通常會在一個階段執行的過程中調用或激活另外一個階段。

了解了Java類的生命周期以後,那麼我們現在來回答第一個問題:虛擬機什麼時候才會載入Class文件並初始化類呢?



1、類載入時機

什麼情況下虛擬機需要開始載入一個類呢?虛擬機規範中並沒有對此進行強制約束,這點可以交給虛擬機的具體實現來自由把握。



2、類初始化時機

那麼,什麼情況下虛擬機需要開始初始化一個類呢?這在虛擬機規範中是有嚴格規定的,虛擬機規範指明有且只有五種情況必須立即對類進行初始化(而這一過程自然發生在載入、驗證、準備之後):

1) 遇到new、getstatic、putstatic或invokestatic這四條位元組碼指令(注意,newarray指令觸發的只是數組類型本身的初始化,而不會導致其相關類型的初始化,比如,new String[]只會直接觸發String[]類的初始化,也就是觸發對類[Ljava.lang.String的初始化,而直接不會觸發String類的初始化)時,如果類沒有進行過初始化,則需要先對其進行初始化。生成這四條指令的最常見的Java代碼場景是:

  • 使用new關鍵字實例化對象的時候;

  • 讀取或設置一個類的靜態欄位(被final修飾,已在編譯器把結果放入常量池的靜態欄位除外)的時候;

  • 調用一個類的靜態方法的時候。


2) 使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。

3) 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。

4) 當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。

5) 當使用jdk1.7動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行初始化,則需要先出觸發其初始化。



注意,對於這五種會觸發類進行初始化的場景,虛擬機規範中使用了一個很強烈的限定語:「有且只有」,這五種場景中的行為稱為對一個類進行主動引用。除此之外,所有引用類的方式,都不會觸發初始化,稱為 被動引用

特別需要指出的是,類的實例化與類的初始化是兩個完全不同的概念:

  • 類的實例化是指創建一個類的實例(對象)的過程;

  • 類的初始化是指為類中各個類成員(被static修飾的成員變數)賦初始值的過程,是類生命周期中的一個階段。


3、被動引用的幾種經典場景

1)、通過子類引用父類的靜態欄位,不會導致子類初始化

JVM類載入機制概述:載入時機與載入過程

JVM類載入機制概述:載入時機與載入過程

對於靜態欄位,只有直接定義這個欄位的類才會被初始化,因此通過其子類來引用父類中定義的靜態欄位,只會觸發父類的初始化而不會觸發子類的初始化。在本例中,由於value欄位是在類SClass中定義的,因此該類會被初始化;此外,在初始化類SClass時,虛擬機會發現其父類SSClass還未被初始化,因此虛擬機將先初始化父類SSClass,然後初始化子類SClass,而SubClass始終不會被初始化。



2)、通過數組定義來引用類,不會觸發此類的初始化

JVM類載入機制概述:載入時機與載入過程

上述案例運行之後並沒有任何輸出,說明虛擬機並沒有初始化類SClass。但是,這段代碼觸發了另外一個名為[Lcn.edu.tju.rico.SClass的類的初始化。從類名稱我們可以看出,這個類代表了元素類型為SClass的一維數組,它是由虛擬機自動生成的,直接繼承於Object的子類,創建動作由位元組碼指令newarray觸發。



3)、常量在編譯階段會存入調用類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化

JVM類載入機制概述:載入時機與載入過程

上述代碼運行之後,只輸出 「hello world」,這是因為雖然在Java源碼中引用了ConstClass類中的常量CONSTANT,但是編譯階段將此常量的值「hello world」存儲到了NotInitialization常量池中,對常量ConstClass.CONSTANT的引用實際都被轉化為NotInitialization類對自身常量池的引用了。也就是說,實際上NotInitialization的Class文件之中並沒有ConstClass類的符號引用入口,這兩個類在編譯為Class文件之後就不存在關係了。



三. 類載入過程

如下圖所示,我們在上文已經提到過一個類的生命周期包括載入(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using) 和 卸載(Unloading)七個階段。現在我們一一學習一下JVM在載入、驗證、準備、解析和初始化五個階段是如何對每個類進行操作的。

JVM類載入機制概述:載入時機與載入過程



1、載入(Loading)

在載入階段(可以參考java.lang.ClassLoader的loadClass()方法),虛擬機需要完成以下三件事情:

(1). 通過一個類的全限定名來獲取定義此類的二進位位元組流(並沒有指明要從一個Class文件中獲取,可以從其他渠道,譬如:網路、動態生成、資料庫等);

(2). 將這個位元組流所代表的靜態存儲結構轉化為方法區的運行時數據結構

(3). 在內存中(對於HotSpot虛擬就而言就是方法區)生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口

載入階段和連接階段(Linking)的部分內容(如一部分位元組碼文件格式驗證動作)是交叉進行的,載入階段尚未完成,連接階段可能已經開始,但這些夾在載入階段之中進行的動作,仍然屬於連接階段的內容,這兩個階段的開始時間仍然保持著固定的先後順序。

特別地,第一件事情(通過一個類的全限定名來獲取定義此類的二進位位元組流)是由類載入器完成的。



2、驗證(Verification)

驗證是連接階段的第一步,這一階段的目的是為了確保Class文件的位元組流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。 驗證階段大致會完成4個階段的檢驗動作:

  • 文件格式驗證:驗證位元組流是否符合Class文件格式的規範(例如,是否以魔術0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理範圍之內、常量池中的常量是否有不被支持的類型)

  • 元數據驗證:對位元組碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範的要求(例如:這個類是否有父類,除了java.lang.Object之外);

  • 位元組碼驗證:通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的;

  • 符號引用驗證:確保解析動作能正確執行。

    驗證階段是非常重要的,但不是必須的,它對程序運行期沒有影響。如果所引用的類經過反覆驗證,那麼可以考慮採用-Xverifynone參數來關閉大部分的類驗證措施,以縮短虛擬機類載入的時間。


3、準備(Preparation)

準備階段是正式為類變數(static 成員變數)分配內存並設置類變數初始值(零值)的階段,這些變數所使用的內存都將在方法區中進行分配。這時候進行內存分配的僅包括類變數,而不包括實例變數,實例變數將會在對象實例化時隨著對象一起分配在堆中。其次,這裡所說的初始值「通常情況」下是數據類型的零值,假設一個類變數的定義為:

JVM類載入機制概述:載入時機與載入過程

那麼,變數value在準備階段過後的值為0而不是123。因為這時候尚未開始執行任何java方法,而把value賦值為123的putstatic指令是程序被編譯後,存放於類構造器方法()之中,所以把value賦值為123的動作將在初始化階段才會執行。至於「特殊情況」是指:當類欄位的欄位屬性是ConstantValue時,會在準備階段初始化為指定的值,所以標註為final之後,value的值在準備階段初始化為123而非0。

JVM類載入機制概述:載入時機與載入過程

4、解析(Resolution)

解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程。解析動作主要針對類或介面、欄位、類方法、介面方法、方法類型、方法句柄和調用點限定符7類符號引用進行。



5、初始化(Initialization)

類初始化階段是類載入過程的最後一步。在前面的類載入過程中,除了在載入階段用戶應用程序可以通過自定義類載入器參與之外,其餘動作完全由虛擬機主導和控制。到了初始化階段,才真正開始執行類中定義的java程序代碼(位元組碼)。

在準備階段,變數已經賦過一次系統要求的初始值(零值);而在初始化階段,則根據程序猿通過程序制定的主觀計劃去初始化類變數和其他資源,或者更直接地說:初始化階段是執行類構造器()方法的過程。()方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊static{}中的語句合并產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊只能訪問到定義在靜態語句塊之前的變數,定義在它之後的變數,在前面的靜態語句塊可以賦值,但是不能訪問。如下:

JVM類載入機制概述:載入時機與載入過程

類構造器()與實例構造器()不同,它不需要程序員進行顯式調用,虛擬機會保證在子類類構造器()執行之前,父類的類構造()執行完畢。由於父類的構造器()先執行,也就意味著父類中定義的靜態語句塊/靜態變數的初始化要優先於子類的靜態語句塊/靜態變數的初始化執行。特別地,類構造器()對於類或者介面來說並不是必需的,如果一個類中沒有靜態語句塊,也沒有對類變數的賦值操作,那麼編譯器可以不為這個類生產類構造器()。

虛擬機會保證一個類的類構造器()在多線程環境中被正確的加鎖、同步,如果多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的類構造器(),其他線程都需要阻塞等待,直到活動線程執行()方法完畢。特別需要注意的是,在這種情形下,其他線程雖然會被阻塞,但如果執行()方法的那條線程退出後,其他線程在喚醒之後不會再次進入/執行()方法,因為 在同一個類載入器下,一個類型只會被初始化一次。如果在一個類的()方法中有耗時很長的操作,就可能造成多個線程阻塞,在實際應用中這種阻塞往往是隱藏的,如下所示:

JVM類載入機制概述:載入時機與載入過程

JVM類載入機制概述:載入時機與載入過程

如上述代碼所示,在初始化DeadLoopClass類時,線程Thread-1得到執行並在執行這個類的類構造器() 時,由於該方法包含一個死循環,因此久久不能退出。



四. 典型案例分析

我們知道,在Java中, 創建一個對象常常需要經歷如下幾個過程:父類的類構造器() -> 子類的類構造器() -> 父類的成員變數和實例代碼塊 -> 父類的構造函數 -> 子類的成員變數和實例代碼塊 -> 子類的構造函數。

那麼,我們看看下面的程序的輸出結果:

JVM類載入機制概述:載入時機與載入過程

JVM類載入機制概述:載入時機與載入過程

大家能得到正確答案嗎?雖然筆者勉強猜出了正確答案,但總感覺怪怪的。因為在初始化階段,當JVM對類StaticTest進行初始化時,首先會執行下面的語句:

JVM類載入機制概述:載入時機與載入過程

也就是實例化StaticTest對象,但這個時候類都沒有初始化完畢啊,能直接進行實例化嗎?事實上,這涉及到一個根本問題就是:實例初始化不一定要在類初始化結束之後才開始初始化。下面我們結合類的載入過程說明這個問題。

我們知道,類的生命周期是:載入->驗證->準備->解析->初始化->使用->卸載,並且只有在準備階段和初始化階段才會涉及類變數的初始化和賦值,因此我們只針對這兩個階段進行分析:

首先,在類的準備階段需要做的是為類變數(static變數)分配內存並設置默認值(零值),因此在該階段結束後,類變數st將變為null、b變為0。特別需要注意的是,如果類變數是final的,那麼編譯器在編譯時就會為value生成ConstantValue屬性,並在準備階段虛擬機就會根據ConstantValue的設置將變數設置為指定的值。也就是說,如果上述程度對變數b採用如下定義方式時:

JVM類載入機制概述:載入時機與載入過程

那麼,在準備階段b的值就是112,而不再是0了。

此外,在類的初始化階段需要做的是執行類構造器(),需要指出的是,類構造器本質上是編譯器收集所有靜態語句塊和類變數的賦值語句按語句在源碼中的順序合并生成類構造器()。因此,對上述程序而言,JVM將先執行第一條靜態變數的賦值語句:

JVM類載入機制概述:載入時機與載入過程

此時,就碰到了筆者上面的疑惑,即「在類都沒有初始化完畢之前,能直接進行實例化相應的對象嗎?」。事實上,從Java角度看,我們知道一個類初始化的基本常識,那就是:在同一個類載入器下,一個類型只會被初始化一次。所以,一旦開始初始化一個類型,無論是否完成,後續都不會再重新觸發該類型的初始化階段了(只考慮在同一個類載入器下的情形)。因此,在實例化上述程序中的st變數時,實際上是把實例初始化嵌入到了靜態初始化流程中,並且在上面的程序中,嵌入到了靜態初始化的起始位置。這就導致了實例初始化完全發生在靜態初始化之前,當然,這也是導致a為110b為0的原因。

因此,上述程序的StaticTest類構造器()的實現等價於:

JVM類載入機制概述:載入時機與載入過程

因此,上述程序會有上面的輸出結果。下面,我們對上述程序稍作改動,如下所示:

JVM類載入機制概述:載入時機與載入過程

在程序最後的一行,增加以下代碼行:

JVM類載入機制概述:載入時機與載入過程

那麼,此時程序的輸出又是什麼呢?如果你對上述的內容理解很好的話,不難得出結論(只有執行完上述代碼行後,StaticTest類才被初始化完成),即:

JVM類載入機制概述:載入時機與載入過程

另外,下面這道經典題目也很有意思,如下:

JVM類載入機制概述:載入時機與載入過程

JVM類載入機制概述:載入時機與載入過程

那麼,這個程序的輸出又是什麼呢?當然,程序跑一下就知道結果。其實,對於這類型題目,我們只要真正理解類的實例化過程,就可以做到所向披靡。


學習Java的同學注意了!!!

學習過程中遇到什麼問題或者想獲取學習資源的話,歡迎加入Java學習交流群495273252,我們一起學Java!

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Java團長 的精彩文章:

Java IO詳解——隨機訪問文件流
學習spring之前,先了解一下代理模式
對《深入理解Java虛擬機》的總結(一)
Java IO詳解——序列化與反序列化(對象流)

TAG:Java團長 |

您可能感興趣

PHP的文件載入
XML DOM 載入函數
Unity資源載入入門
一場載入史冊的DNA大通緝
為加速網頁載入 谷歌試圖讓AMP標準化
遊戲載入時間縮減65% XBOX主機就該用SSD
從輸入URL到頁面載入發生了什麼?
第一次載入武器系統的F-35戰機進行測試,你知道是A還是B么?
解讀MySQL驅動載入邏輯
測試MIUI10的AI預載入功能!另外小米6X也將加入!
給PS4外接個移動SSD,遊戲載入快到飛起
裝機員幫你解決win7系統電腦開機一直卡在正在載入個人設置的界面
谷歌黑科技:AMP技術讓網頁載入更快速
SSD對遊戲載入有用嗎?我們測試三款遊戲
10件值得被載入設計史中的IKEA傢具
谷歌努力推進AMP標準化,成功可加速網頁載入
NBA球員關鍵時刻腦袋短路,JR載入史冊,隆多空籃都不上!
Linux命令備忘錄:mount用於載入文件系統到指定的載入點
Xbox One X主機換裝SSD:遊戲啟動、載入大提速
《最終幻想15》首個MOD下載 修復載入過長問題