《深入理解java虛擬機器》學習筆記7——Java虛擬機器類生命週期

yangxi_001發表於2013-12-04

C/C++等純編譯語言從原始碼到最終執行一般要經歷:編譯、連線和執行三個階段,連線是在編譯期間完成,而java在編譯期間僅僅是將原始碼編譯為Java虛擬機器可以識別的位元組碼Class類檔案,Java虛擬機器對中Class類檔案的載入、連線都在執行時執行,雖然類載入和連線會佔用程式的執行時間增加效能開銷,但是卻可以為java語言帶來高度靈活性和擴充套件性,java的針對介面程式設計和類載入器機制實現的OSGi以及熱部署等就是利用了執行時類載入和連線的特性,java的Class類在虛擬機器中的生命週期如下:

上圖中載入、驗證、準備、初始化和解除安裝這個五個階段的順序是確定的,而解析階段則不一定,在某些情況下為了支援java語言的執行時動態繫結,也可以在初始化階段之後再開始。

(1).載入:

Java虛擬機器把Class類檔案載入到記憶體中,並對Class檔案中的資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的java型別的過程。

在載入階段,java虛擬機器需要完成以下3件事:

a.通過一個類的全限定名來獲取定義此類的二進位制位元組流。

b.將定義類的二進位制位元組流所代表的靜態儲存結構轉換為方法區的執行時資料結構。

c.在java堆中生成一個代表該類的java.lang.Class物件,作為方法區資料的訪問入口。

載入階段與連線階段是交叉進行的,載入階段尚未完成,連線階段可能已經開始,這些夾在載入階段之中進行的動作仍然屬於連線階段,載入和連線階段仍然保持著固定的先後順序。

(2).驗證:

驗證是連線階段的第一步,其目的是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器的安全,如果驗證失敗,會丟擲java.lang.VerifyError異常。

驗證階段的主要工作有:

a.檔案格式驗證:驗證Class檔案魔數、主次版本、常量池、類檔案本身等等。

b.後設資料驗證:主要是對位元組碼描述的資訊進行語義分析,包括是否有父類、是否是抽象類、是否是介面、是否繼承了不允許被繼承的類(final類)、是否實現了父類或者介面的方法等等。

c.位元組碼驗證:是整個驗證過程中最複雜的,主要進行資料流和控制流分析,如保證跳轉指令不會跳轉到方法體之外的位元組碼指令、資料型別轉換安全有效等。

d.符號引用驗證:發生在虛擬機器將符號引用轉化為直接引用的時候(連線第三階段-解析階段進行符號引用轉換為直接引用),符號引用驗證的目的是確保解析動作能正常執行,如果無法通過符號引用驗證,則會丟擲java.lang.IncompatibleClassChangeError異常的子類異常,如java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。

驗證階段對於虛擬機器來說非常重要,但是不是一個必需的階段,如果所執行的程式碼已經反覆被使用和驗證過了,可以通過-Xverify:none引數關閉大部分的驗證措施,以提高虛擬機器時間時間。

(3).準備:

準備階段是正式為類變數(靜態變數,注意不是例項變數)分配記憶體並設定類變數初始值的階段,這些記憶體都將在方法區中進行分配。

對於普通非final的類變數,如public static int value = 123;在準備階段過後的初始值是0(資料型別的零值),而不是123,而把123賦值給value是在初始化階段才進行的動作。

對於final的類變數,即常量,如public staticfinal int value =123;在準備階段過程的初始值直接就是123了,不需要準備為零值。

(4).解析:

解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程。

符號引用(SymbolicReference):以一組符號來描述所引用的目標,與虛擬機器記憶體佈局無關,引用的目標不一定已經被載入到虛擬機器記憶體中。

直接引用(DirectReference):可以直接指向目標的指標、相對偏移量或者是一個能間接定位到目標的控制程式碼。直接引用和虛擬機器實現的記憶體佈局相關,同一個符號引用在不同虛擬機器上翻譯處理的直接引用不一定相同,如果有了直接引用,則引用的目標物件必須已經被載入到虛擬機器記憶體中。

解析的動作主要針對類或介面、欄位、類方法、介面方法四類符號引用進行解析。

(5).初始化:

初始化是類使用前的最後一個階段,在初始化階段java虛擬機器真正開始執行類中定義的java程式程式碼。

Java虛擬機器規範嚴格規定了有且只有以下四種情況必須立即對類進行初始化:

a.遇到new、獲取靜態變數(final常量除外)、為靜態變數賦值以及呼叫靜態方法時,如果類沒有進行過初始化,則需要先觸發其初始化。

b.使用java.lang.reflect包的方法對類進行反射呼叫的時候(Class.forName(…)),如果類還沒有初始化,需要先觸發對其的初始化。

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

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

上述四種情況稱為對一個類的主動引用,除此之外的引用方式都不會觸發初始化,稱為被動引用。

初始化的過程其實就是一個執行類構造器<clint>方法的過程,類構造器執行的特點和注意事項:

1).類構造器<clint>方法是由編譯器自動收集類中所有類變數(靜態非final變數)賦值動作和靜態初始化塊(static{……})中的語句合併產生的,編譯器收集的順序是由語句在原始檔中出現的順序決定。靜態初始化塊中只能訪問到定義在它之前的類變數,定義在它之後的類變數,在前面的靜態初始化中可以賦值,但是不能訪問。

2).類構造器<clint>方法與例項構造器<init>方法不同,它不需要顯式地呼叫父類構造器方法,虛擬機器會保證在呼叫子類構造器方法之前,父類的構造器<clinit>方法已經執行完畢。

3).由於父類構造器<clint>方法先與子類構造器執行,因此父類中定義的靜態初始化塊要先於子類的類變數賦值操作。

4). 類構造器<clint>方法對於類和介面並不是必須的,如果一個類中沒有靜態初始化塊,也沒有類變數賦值操作,則編譯器可以不為該類生成類構造器<clint>方法。

5).介面中不能使用靜態初始化塊,但可以有類變數賦值操作,因此介面與類一樣都可以生成類構造器<clint>方法。

介面與類不同的是:

首先,執行介面的類構造器<clint>方法時不需要先執行父介面的類構造器<clint>方法,只有當父介面中定義的靜態變數被使用時,父介面才會被初始化。

其次,介面的實現類在初始化時同樣不會執行介面的類構造器<clint>方法。

6).java虛擬機器會保證一個類的<clint>方法在多執行緒環境中被正確地加鎖和同步,如果多個執行緒同時去初始化一個類,只會有一個執行緒去執行這個類的<clint>方法,其他執行緒都需要阻塞等待,直到活動執行緒執行<clint>方法完畢。

初始化階段,當執行完類構造器<clint>方法之後,才會執行例項構造器的<init>方法,例項構造方法同樣是按照先父類,後子類,先成員變數,後例項構造方法的順序執行。
(6).使用:

當初始化完成之後,java虛擬機器就可以執行Class的業務邏輯指令,通過堆中java.lang.Class物件的入口地址,呼叫方法區的方法邏輯,最後將方法的運算結果通過方法返回地址存放到方法區或堆中。

(7).解除安裝:

當物件不再被使用時,java虛擬機器的垃圾收集器將會回收堆中的物件,方法區中不再被使用的Class也要被解除安裝,否則方法區(Sun HotSpot永久代)會記憶體溢位。

Java虛擬機器規定只有當載入該型別的類載入器例項為unreachable狀態時,當前被載入的型別才被解除安裝.啟動類載入器例項永遠為reachable狀態,由啟動類載入器載入的型別可能永遠不會被解除安裝,型別解除安裝僅僅是作為一種減少記憶體使用的效能優化措施存在的,具體和虛擬機器實現有關,對開發者來說是透明的.

解除安裝自定義來載入器載入的類的可靠做法為:

a.每次建立特定類載入器的新例項來載入指定型別的不同版本,這種使用場景下,一般就要犧牲快取特定型別的類載入器例項以帶來效能優化的策略了.

b.對於指定型別已經被載入的版本, 會在適當時機達到unreachable狀態,被unload並垃圾回收.每次使用完類載入器特定例項後(確定不需要再使用時), 將其顯示賦為null, 這樣可能會比較快的達到jvm 規範中所說的類載入器例項unreachable狀態, 增大已經不再使用的型別版本被儘快解除安裝的機會.

相關文章