我是家寶
虛擬機把描述類的數據從Class文件加載到內存,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。
類的加載過程分為5個步驟:加載、驗證、准備、解析、初始化
其中的驗證、准備、解析階段又統稱為連接,如下圖所示。
在這5個階段中,加載、驗證、准備、初始化這4個階段的順序是確定的,類的加載過程必須按照這種順序按部就班地開始,而解析階段則不一定,為了支持java語言的運行時綁定,它在某些情況下可以在初始化階段之後再開始。
這裡之所以說按部就班地開始,而不是按部就班地“進行”或“完成”,是因為這些階段通常可以交叉混合進行,如在加載階段執行過程中,也會同時執行驗證階段。
什麼時候要開始一個類的類加載?
什麼情況下需要開始類加載過程的第一步“加載”?Java虛擬機並沒有進行強制約束,這點可以由虛擬機自行實現。
但是對於初始化階段,虛擬機規范則進行了嚴格規定,有且只有在以下7種情況下,必須立即對類進行初始化(而加載、驗證、准備自然需要在此之前開始):
在加載階段,虛擬機需要完成以下3件事情:
驗證階段大致分4個階段:
此階段的驗證是基於二進制字節流進行的,只有通過了這個階段的驗證後,字節流才會進入內存的方法區中進行存儲,所以後面3個階段全部是基於方法區的存儲結構進行的,不再直接操作字節流。(從這裡也可以看出,驗證階段是和加載階段一起進行的,只有當驗證階段完成後,加載階段的第二個步驟將字節流轉儲為方法區的運行時數據結構才能完成)
准備階段是正式為類變量分配內存並設置類變量初始值。
需要注意的是,類似於:
“public static int value=123” 這種定義,在准備階段過後的初始值是0而不是123,把value賦值為123需要等到初始化階段再執行。
但是如果是:
“public static final int value=123” 這種定義,編譯時javac會給value生成ConstantValue屬性,這種情況下在准備階段虛擬機就會根據ConstantValue將value賦值為123。
解析階段是虛擬機將常量池內的符號引用轉化為直接引用的過程。
主要有4種:
初始化階段是執行類構造器<clinit>()方法的過程。
<clinit>()方法是由編譯器自動收集類中的靜態變量賦值動作和靜態語句塊(static{})中的語句合並生成的,編譯器的收集順序由語句在源文件中出現的順序決定。
需要注意的有兩點:
在類加載的第一個階段--“加載”階段,第一個動作是:“通過一個類的全限定名來獲取此類的二進制字節流”,我們把實現這個動作的代碼模塊稱為“類加載器”。
類加載器最初是為了滿足Java Applet的需求而開發的,雖然目前Java Applet基本已經“死掉”,但是類加載器卻在類層次劃分、OSGi、熱部署、代碼加密等領域大放異彩,成為Java體系中的一塊重要基石。
必須了解的一個概念是:
對於一個類,這個類本身和加載它的類加載器一同確立其在Java虛擬機的唯一性。
也就是說,不同類加載器加載同一個Class文件所產生的兩個類是不相等的,體現在Class對象的equals()方法、instanceof關鍵字的判定等。
如圖:
圖中的類加載器之間的層次關系,稱為類加載器的雙親委派模型。
雙親委派模型要求除了頂層的啟動類加載器之外,其他的加載器都要有自己的父加載器。這裡類加載器之間的父子關系不以繼承而是以組合方式來實現。
雙親委派模型的工作過程是:每一個類加載器收到類加載請求,都會首先將請求委派到其父加載器去完成,只有當父加載器無法完成加載,子加載器才會嘗試自己去加載。
雙親委派模型的好處是Java類隨著它的加載器一起具備了優先級的層級關系。如java.lang.Object,它存在於rt.jar,無論哪個類加載器要加載這個類,最終都要委派給頂層的啟動加載器,因此Object在程序的各類加載器環境中都是同一個類。即使用戶自己定義一個java.lang.Object類,也無法被加載。