聯想實驗場之JVM中類的生命週期詳解

1. 概述

本文是在看了很多的關於JVM和類加載器的相關資料後的一個簡要總結,後文也會給出相關的參考地址。

JVM類加載機制這裡準備分兩篇文章來分別介紹,一片主要介紹jvm中類的生命週期,另一篇著重講一下類加載器和反射。單獨講解類加載器是因為 類加載這部分是唯一我們可以通過自己的代碼程序進行干預的部分,而其他部分都是jvm內部直接完成的。而本文主要是前一部分,即JVM中類的生命週期。

在開始正文前這裡先來看兩張圖

下圖所示為java程序的執行流程圖

聯想實驗場之JVM中類的生命週期詳解

下圖所示為Jvm物理結構圖

聯想實驗場之JVM中類的生命週期詳解

上面第一張圖是Java程序的執行流程圖,具體的過程就是源碼編碼過程,編譯過程,還有類裝載過程,和解釋執行。本文主要的內容就是從類裝載到程序執行的這個中間的所有過程。第二張圖是JVM的結構圖,在說明類加載的過程的時候很難避免這張圖裡面的內存空間那部分,關於具體的JMM和GC部分就不在這裡涉及了。

2. 類加載機制概念

Java虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的加載機制

Class類和Class文件Class文件

Class文件是在Java源碼編譯後生成的字節碼文件,由類裝載器裝載後,在JVM中將形成一份描述Class結構的元信息對象,通過該元信息對象可以獲知Class的結構信息:如構造函數,屬性和方法等,Java允許用戶藉由這個Class相關的元信息對象間接調用Class對象的功能,這裡就是我們經常能見到的Class類

類從被加載到虛擬機內存中開始,到卸載出內存為止,它的整個生命週期包括了:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(using)、和卸載(Unloading)七個階段。其中驗證、準備和解析三個部分統稱為連接(Linking),這七個階段的發生順序我們用一行圖來說明。

下圖所示為類加載的生命週期圖

聯想實驗場之JVM中類的生命週期詳解

3. 類加載的工作機制

類裝載器就是尋找類的字節碼文件,並構造出類在JVM內部表示的對象組件。在Java中,類裝載器把一個類裝入JVM中,要經過以下步驟,為了突出我做成了一張圖:

聯想實驗場之JVM中類的生命週期詳解

Java程序可以動態擴展是由運行期動態加載和動態鏈接實現的;比如:如果編寫一個使用接口的應用程序,可以等到運行時再指定其實際的實現(多態),解析過程有時候還可以在初始化之後執行;比如:動態綁定(多態)

如類加載的生命週期圖所示,加載、驗證、準備、初始化和卸載這五個階段的順序是確定的,類的加載過程必須按照這個順序來按部就班地開始,而解析階段則不一定,它在某些情況下可以在初始化階段後再開始。類的生命週期的每一個階段通常都是互相交叉混合式進行的,通常會在一個階段執行的過程中調用或激活另外一個階段。

4. 類加載生命週期的詳細介紹

在我參考別人的資料的時候,發現都是先介紹的類的初始化這個過程,但是我覺得這樣會造成一種誤解什麼事類的初始化,和它應該發生的時機。這裡我就按照類的加載生命週期順序介紹每一個過程了。

(1) 裝載(加載)

什麼是類的裝載

類的裝載指的是將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,然後在堆區創建一個java.lang.Class對象,用來封裝類在方法區內的數據結構。類的加載的最終產品是位於堆區中的Class對象,Class對象封裝了類在方法區內的數據結構,並且向Java程序員提供了訪問方法區內的數據結構的接口。

聯想實驗場之JVM中類的生命週期詳解

類加載器並不需要等到某個類被“首次主動使用”時再加載它,JVM規範允許類加載器在預料某個類將要被使用時就預先加載它,如果在預先加載的過程中遇到了.class文件缺失或存在錯誤,類加載器必須在程序首次主動使用該類時才報告錯誤(LinkageError錯誤)如果這個類一直沒有被程序主動使用,那麼類加載器就不會報告錯誤。

加載.class文件的方式有:

聯想實驗場之JVM中類的生命週期詳解

在瞭解了什麼是類的加載後,回頭來再看jvm進行類加載階段都做了什麼。虛擬機需要完成以下三件事情:

聯想實驗場之JVM中類的生命週期詳解

相對於類加載過程的其他階段,加載階段是開發期相對來說可控性比較強,該階段既可以使用系統提供的類加載器完成,也可以由用戶自定義的類加載器來完成,開發人員可以通過定義自己的類加載器去控制字節流的獲取方式。關於這個過程的更多細節,我會在下一節細說,類的加載。

加載階段完成後,虛擬機外部的 二進制字節流就按照虛擬機所需的格式存儲在方法區之中,而且在Java堆中也創建一個java.lang.Class類的對象,這樣便可以通過該對象訪問方法區中的這些數據。

(2) 驗證

驗證的目的是為了確保Class文件中的字節流包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。不同的虛擬機對類驗證的實現可能會有所不同,但大致都會完成以下四個階段的驗證:文件格式的驗證、元數據的驗證、字節碼驗證和符號引用驗證。

1)文件格式的驗證:驗證字節流是否符合Class文件格式的規範,並且能被當前版本的虛擬機處理,該驗證的主要目的是保證輸入的字節流能正確地解析並存儲於方法區之內。經過該階段的驗證後,字節流才會進入內存的方法區中進行存儲,後面的三個驗證都是基於方法區的存儲結構進行的。

2)元數據驗證:對類的元數據信息進行語義校驗(其實就是對類中的各數據類型進行語法校驗),保證不存在不符合Java語法規範的元數據信息。

3)字節碼驗證:該階段驗證的主要工作是進行數據流和控制流分析,對類的方法體進行校驗分析,以保證被校驗的類的方法在運行時不會做出危害虛擬機安全的行為。

4)符號引用驗證:這是最後一個階段的驗證,它發生在虛擬機將符號引用轉化為直接引用的時候(解析階段中發生該轉化,後面會有講解),主要是對類自身以外的信息(常量池中的各種符號引用)進行匹配性的校驗。

(3) 準備

準備階段是正式為類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中進行分配。

1)這時候進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨著對象一塊分配在Java堆中。

2)這裡所設置的初始值通常情況下是數據類型默認的零值(如0、0L、null、false等),而不是被在Java代碼中被顯式地賦予的值。

(4) 解析

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

符號引用(Symbolic Reference):符號引用以一組符號來描述所引用的目標,符號引用可以是任何形式的字面量,符號引用與虛擬機實現的內存佈局無關,引用的目標並不一定已經在內存中。

直接引用(Direct Reference) :直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機實現的內存佈局相關的,同一個符號引用在不同的虛擬機實例上翻譯出來的直接引用一般都不相同,如果有了直接引用,那引用的目標必定已經在內存中存在。

1、類或接口的解析:判斷所要轉化成的直接引用是對數組類型,還是普通的對象類型的引用,從而進行不同的解析。

2、字段解析:對字段進行解析時,會先在本類中查找是否包含有簡單名稱和字段描述符都與目標相匹配的字段,如果有,則查找結束;如果沒有,則會按照繼承關係從上往下遞歸搜索該類所實現的各個接口和它們的父接口,還沒有,則按照繼承關係從上往下遞歸搜索其父類,直至查找結束。

3、類方法解析:對類方法的解析與對字段解析的搜索步驟差不多,只是多了判斷該方法所處的是類還是接口的步驟,而且對類方法的匹配搜索,是先搜索父類,再搜索接口。

4、接口方法解析:與類方法解析步驟類似,只是接口不會有父類,因此,只遞歸向上搜索父接口就行了。

(5) 初始化

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

初始化,為類的靜態變量賦予正確的初始值,JVM負責對類進行初始化,主要對類變量進行初始化。在Java中對類變量進行初始值設定有兩種方式:

①聲明類變量時指定初始值

②使用靜態代碼塊為類變量指定初始值

JVM初始化步驟

1、假如這個類還沒有被加載和連接,則程序先加載並連接該類

2、假如該類的直接父類還沒有被初始化,則先初始化其直接父類

3、假如類中有初始化語句,則系統依次執行這些初始化語句

初始化階段時執行類構造器()方法的過程。

1)<clinit>()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static{}塊)中的語句合併產生的,編譯器收集的順序由語句在源文件中出現的順序所決定。

2)<clinit>()方法與類的構造函數不同,它不需要顯式地調用父類構造器,虛擬機會保證在子類的<clinit>()方法執行之前,父類的<clinit>()方法已經執行完畢,因此在虛擬機中第一個執行的<clinit>()方法的類一定是java.lang.Object。

3)由於父類的<clinit>()方法先執行,也就意味著父類中定義的靜態語句塊要優先於子類的變量賦值操作。

4)<clinit>()方法對於類或者接口來說並不是必需的,如果一個類中沒有靜態語句塊也沒有對變量的賦值操作,那麼編譯器可以不為這個類生成<clinit>()方法。

5)接口中可能會有變量賦值操作,因此接口也會生成<clinit>()方法。但是接口與類不同,執行接口的<clinit>()方法不需要先執行父接口的<clinit>()方法。只有當父接口中定義的變量被使用時,父接口才會被初始化。另外,接口的實現類在初始化時也不會執行接口的<clinit>()方法。

6)虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確地加鎖和同步。如果有多個線程去同時初始化一個類,那麼只會有一個線程去執行這個類的<clinit>()方法,其它線程都需要阻塞等待,直到活動線程執行<clinit>()方法完畢。如果在一個類的<clinit>()方法中有耗時很長的操作,那麼就可能造成多個進程阻塞。

類初始化的觸發條件:只有當對類的主動使用的時候才會導致類的初始化。

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

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

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

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

只有上述四種情況會觸發初始化,也稱為對一個類進行主動引用,除此以外,所有其他方式都不會觸發初始化,稱為被動引用。

關於上面的這四種說法,換一種通俗的解釋應該對應下面的六種:

(1) 創建類的實例,也就是new的方式

(2) 訪問某個類或接口的靜態變量,或者對該靜態變量賦值

(3) 調用類的靜態方法

(4) 反射(如Class.forName(“com.play.Test”))

(5) 初始化某個類的子類,則其父類也會被初始化

(6) Java虛擬機啟動時被標明為啟動類的類(Java Test),直接使用java.exe命令來運行某個主類

結束生命週期

在以下情況的時候,Java虛擬機會結束生命週期

1. 執行了System.exit()方法

2. 程序正常執行結束

3. 程序在執行過程中遇到了異常或錯誤而異常終止

4. 由於操作系統出現錯誤而導致Java虛擬機進程終止

5. 參考資料

http://www.cnblogs.com/ityouknow/p/5603287.html

http://smallbug-vip.iteye.com/blog/2275284

http://www.cnblogs.com/ITtangtang/p/3978102.html

http://blog.csdn.net/gjanyanlig/article/details/6818655

作者:Hammer

相關推薦

推薦中...