天天看點

Java虛拟機類加載機制

看到這個題目,很多人會覺得我寫我的java代碼,至于類,jvm愛怎麼加載就怎麼加載,部落客有很長一段時間也是這麼認為的。随着程式設計經驗的日積月累,越來越感覺到了解虛拟機相關要領的重要性。閑話不多說,老規矩,先來一段代碼吊吊胃口。

運作結果:

答案答對了嚒? 

也許有人會疑問:為什麼沒有輸出subclass init。ok~解釋一下:對于靜态字段,隻有直接定義這個字段的類才會被初始化,是以通過其子類來引用父類中定義的靜态字段,隻會觸發父類的初始化而不會觸發子類的初始化。 如果你想學習java可以來這個群,首先是一二六,中間是五三四,最後是五一九,裡面有大量的學習資料可以下載下傳。

上面就牽涉到了虛拟機類加載機制。如果有興趣,可以繼續看下去。

類加載過程

類從被加載到虛拟機記憶體中開始,到解除安裝出記憶體為止,它的整個生命周期包括:加載(loading)、驗證(verification)、準備(preparation)、解析(resolution)、初始化(initialization)、使用(using)和解除安裝(unloading)7個階段。其中準備、驗證、解析3個部分統稱為連接配接(linking)。如圖所示。

Java虛拟機類加載機制

加載、驗證、準備、初始化和解除安裝這5個階段的順序是确定的,類的加載過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之後再開始,這是為了支援java語言的運作時綁定(也稱為動态綁定或晚期綁定)。以下陳述的内容都已hotspot為基準。

加載

在加載階段(可以參考java.lang.classloader的loadclass()方法),虛拟機需要完成以下3件事情:

1.通過一個類的全限定名來擷取定義此類的二進制位元組流(并沒有指明要從一個class檔案中擷取,可以從其他管道,譬如:網絡、動态生成、資料庫等); 

2.将這個位元組流所代表的靜态存儲結構轉化為方法區的運作時資料結構; 

3.在記憶體中生成一個代表這個類的java.lang.class對象,作為方法區這個類的各種資料的通路入口; 

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

驗證

驗證是連接配接階段的第一步,這一階段的目的是為了確定class檔案的位元組流中包含的資訊符合目前虛拟機的要求,并且不會危害虛拟機自身的安全。 

驗證階段大緻會完成4個階段的檢驗動作:

1.檔案格式驗證:驗證位元組流是否符合class檔案格式的規範;例如:是否以魔術0xcafebabe開頭、主次版本号是否在目前虛拟機的處理範圍之内、常量池中的常量是否有不被支援的類型。 

2.中繼資料驗證:對位元組碼描述的資訊進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的資訊符合java語言規範的要求;例如:這個類是否有父類,除了java.lang.object之外。 

3.位元組碼驗證:通過資料流和控制流分析,确定程式語義是合法的、符合邏輯的。 

4.符号引用驗證:確定解析動作能正确執行。

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

準備

準備階段是正式為類變量配置設定記憶體并設定類變量初始值的階段,這些變量所使用的記憶體都将在方法區中進行配置設定。這時候進行記憶體配置設定的僅包括類變量(被static修飾的變量),而不包括執行個體變量,執行個體變量将會在對象執行個體化時随着對象一起配置設定在堆中。其次,這裡所說的初始值“通常情況”下是資料類型的零值,假設一個類變量的定義為:

那變量value在準備階段過後的初始值為0而不是123.因為這時候尚未開始執行任何java方法,而把value指派為123的putstatic指令是程式被編譯後,存放于類構造器()方法之中,是以把value指派為123的動作将在初始化階段才會執行。 

至于“特殊情況”是指:public static final int value=123,即當類字段的字段屬性是constantvalue時,會在準備階段初始化為指定的值,是以标注為final之後,value的值在準備階段初始化為123而非0.

解析

解析階段是虛拟機将常量池内的符号引用替換為直接引用的過程。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符号引用進行。

初始化

類初始化階段是類加載過程的最後一步,到了初始化階段,才真正開始執行類中定義的java程式代碼。在準備極端,變量已經付過一次系統要求的初始值,而在初始化階段,則根據程式猿通過程式制定的主管計劃去初始化類變量和其他資源,或者說:初始化階段是執行類構造器()方法的過程. 

()方法是由編譯器自動收集類中的所有類變量的指派動作和靜态語句塊static{}中的語句合并産生的,編譯器收集的順序是由語句在源檔案中出現的順序所決定的,靜态語句塊隻能通路到定義在靜态語句塊之前的變量,定義在它之後的變量,在前面的靜态語句塊可以指派,但是不能通路。如下:

()方法與執行個體構造器()方法不同,它不需要顯示地調用父類構造器,虛拟機會保證在子類()方法執行之前,父類的()方法方法已經執行完畢,回到本文開篇的舉例代碼中,結果會列印輸出:ssclass就是這個道理。 

由于父類的()方法先執行,也就意味着父類中定義的靜态語句塊要優先于子類的變量指派操作。 

()方法對于類或者接口來說并不是必需的,如果一個類中沒有靜态語句塊,也沒有對變量的指派操作,那麼編譯器可以不為這個類生産()方法。 

接口中不能使用靜态語句塊,但仍然有變量初始化的指派操作,是以接口與類一樣都會生成()方法。但接口與類不同的是,執行接口的()方法不需要先執行父接口的()方法。隻有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實作類在初始化時也一樣不會執行接口的()方法。 

虛拟機會保證一個類的()方法在多線程環境中被正确的加鎖、同步,如果多個線程同時去初始化一個類,那麼隻會有一個線程去執行這個類的()方法,其他線程都需要阻塞等待,直到活動線程執行()方法完畢。如果在一個類的()方法中有好事很長的操作,就可能造成多個線程阻塞,在實際應用中這種阻塞往往是隐藏的。

運作結果:(即一條線程在死循環以模拟長時間操作,另一條線程在阻塞等待)

需要注意的是,其他線程雖然會被阻塞,但如果執行()方法的那條線程退出()方法後,其他線程喚醒之後不會再次進入()方法。同一個類加載器下,一個類型隻會初始化一次。 

将上面代碼中的靜态塊替換如下:

虛拟機規範嚴格規定了有且隻有5中情況(jdk1.7)必須對類進行“初始化”(而加載、驗證、準備自然需要在此之前開始):

1.遇到new,getstatic,putstatic,invokestatic這失調位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的java代碼場景是:使用new關鍵字執行個體化對象的時候、讀取或設定一個類的靜态字段(被final修飾、已在編譯器把結果放入常量池的靜态字段除外)的時候,以及調用一個類的靜态方法的時候。 

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

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

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

5.當使用jdk1.7動态語言支援時,如果一個java.lang.invoke.methodhandle執行個體最後的解析結果ref_getstatic,ref_putstatic,ref_invokestatic的方法句柄,并且這個方法句柄所對應的類沒有進行初始化,則需要先出觸發其初始化。

開篇已經舉了一個範例:通過子類引用付了的靜态字段,不會導緻子類初始化。 

這裡再舉兩個例子。 

1. 通過數組定義來引用類,不會觸發此類的初始化:(superclass類已在本文開篇定義)

運作結果:(無) 

2. 常量在編譯階段會存入調用類的常量池中,本質上并沒有直接引用到定義常量的類,是以不會觸發定義常量的類的初始化:

運作結果:hello world