JVM類加載機制——類的生命週期

JVM類加載機制——類的生命週期

什麼是類加載機制?

虛擬機把描述類的數據文件(字節碼)加載到內存,並對數據進行驗證、準備、解析以及類初始化,最終造成能夠被虛擬機直接使用的java類型(java.lang.Class對象),這就是java虛擬機的類加載機制。——《 深刻理解java虛擬機》java

類的生命週期

從類被加載進內存開始直到卸載出內存爲止,類的生命週期包括裝載、驗證、準備、解析、初始化、使用和卸載 7個過程,其中驗證、準備、解析三個過程統稱爲連接。程序員

其中類加載的過程包括了加載、驗證、準備、解析、初始化五個階段。在這五個階段中,加載、驗證、準備和初始化這四個階段發生的順序是肯定的,而解析階段則不必定,它在某些狀況下能夠在初始化階段以後開始,這是爲了支持Java語言的運行時綁定(也成爲動態綁定或晚期綁定)。另外注意這裏的幾個階段是按順序開始,而不是按順序進行或完成,由於這些階段一般都是互相交叉地混合進行的,一般在一個階段執行的過程當中調用或激活另外一個階段(如:主動使用類觸發類的初始化)。數據庫

在Java中,類的加載和連接過程都是在程序運行期間完成的。另外,Java能夠動態擴展的語言特性就是依賴運行期間動態加載、動態連接這個特色實現的。數組

接下來咱們詳細瞭解下類的整個生命週期安全

1、裝載(加載)

在加載階段,虛擬機完成3件事:bash

  1. 經過一個類的全限定名來獲取定義此類的二進制字節流。網絡

    二進制字節流除了從本地classpath獲取,還能夠從哪些地方獲取?

    從ZIP或jar包中讀取
    從網絡中獲取
    運行時計算生成(Java動態代理技術)
    由其餘文件生成(由JSP文件生成對應的Class類)
    從數據庫中讀取
    複製代碼
  2. 加二進制字節流存儲在方法區中(按照虛擬機所需的格式存儲)。數據結構

  3. 在內存(堆區)中生成一個表明這個類的java.lang.Class對象,Class對象封裝了類在方法區內的數據結構,而且向Java程序員提供了訪問方法區內的數據結構的接口。多線程

2、驗證

驗證的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。 通常包括兩個方面:jvm

  1. 格式語義校驗:
    例如:
    1.是否以0xCAFEBASE開頭
    2.主、次版本號是否在當前虛擬機處理範圍內
    複製代碼
  2. 代碼邏輯校驗

3、準備

準備階段正式爲靜態變量分配內存並設置初始值,這些靜態變量在方法區中分配內存。

注意:

  1. 準備階段,JVM只會爲靜態變量(static修飾)分配內存,不包括實例變量,實例變量將會在對象實例化時隨對象一塊兒分配在Java堆中。

    準備階段,只會爲value分配內存,不會爲name分配內存
    public static int value = 123;
    private String name = "Tom";
    複製代碼
  2. 設置初始值:

  • static修飾的變量(無final):零值或null

準備階段,未執行任何Java方法,而value賦值爲123指令是程序編譯後,存放於類構造器方法中,在初始化階段纔會執行,所以準備階段,會設置零值。

準備階段,會設置零值
public static int value = 123;
複製代碼
  • static final修飾的常量:實際值
常量,準備階段會設置實際值123
public static final int value = 123;
複製代碼

注意:static final修飾的基本數據類型或者String會在javac編譯時生成ConstantValue屬性,在類加載的準備階段直接把ConstantValue的值賦給該字段。能夠理解爲在編譯期即把結果放入了常量池中。因此當A類調用B類的static final字段(基本數據類型或者String),不會觸發B類的加載。

4、解析

解析階段是虛擬機將常量池中的符號引用轉化爲直接引用的過程 (在某些狀況下能夠在初始化階段以後開始) 解析動做主要針對類或接口、字段、類方法、接口方法四類符號引用進行,分別對應於常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info四種常量類型。

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

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

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

四、接口方法解析:與類方法解析步驟相似,只是接口不會有父類,所以,只遞歸向上搜索父接口就好了。

看以下例子:

class Super{
	public static int m = 11;
	static{
		System.out.println("執行了super類靜態語句塊");
	}
}
 
class Father extends Super{
	public static int m = 33;
	static{
		System.out.println("執行了父類靜態語句塊");
	}
}
 
class Child extends Father{
	static{
		System.out.println("執行了子類靜態語句塊");
	}
}
 
public class StaticTest{
	public static void main(String[] args){
		System.out.println(Child.m);
	}
}
複製代碼

輸出結果:

執行了super類靜態語句塊
執行了父類靜態語句塊
33
複製代碼

爲什子類的static塊不會執行?

static塊是在初始化階段執行的,而static變量發生在靜態解析階段,也便是初始化以前,此時已經將字段的符號引用轉化爲了內存引用,也便將它與對應的類關聯在了一塊兒,因爲在子類中沒有查找到與m相匹配的字段,那麼m便不會與子類關聯在一塊兒,所以並不會觸發子類的初始化。
複製代碼

同理,若是註釋掉Father類中對m定義的那一行,則輸出結果以下:

執行了super類靜態語句塊
11
複製代碼

5、初始化

初始化是類加載過程的最後一步,到了此階段,才真正開始執行類中定義的Java程序代碼。在準備階段,類變量已經被賦過一次系統要求的初始值,而在初始化階段,則是根據程序員經過程序指定的主觀計劃去初始化類變量和其餘資源,或者能夠從另外一個角度來表達:初始化階段是執行類構造器<clinit>()方法的過程。

這裏簡單說明下()方法的執行規則:

  1. <clinit>()方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊以前的變量,定義在它以後的變量,在前面的靜態語句中能夠賦值,可是不能訪問

  2. <clinit>()方法與實例構造器<init>()方法(類的構造函數)不一樣,它不須要顯式地調用父類構造器,虛擬機會保證在子類的<clinit>()方法執行以前,父類的<clinit>()方法已經執行完畢。所以,在虛擬機中第一個被執行的<clinit>()方法的類確定是java.lang.Object

  3. <clinit>()方法對於類或接口來講並非必須的,若是一個類中沒有靜態語句塊,也沒有對類變量的賦值操做,那麼編譯器能夠不爲這個類生成<clinit>()方法。

  4. 接口中不能使用靜態語句塊,但仍然有類變量(final static)初始化的賦值操做,所以接口與類同樣會生成<clinit>()方法。可是接口與類不一樣的是:執行接口的<clinit>()方法不須要先執行父接口的<clinit>()方法,只有當父接口中定義的變量被使用時,父接口才會被初始化。另外,接口的實現類在初始化時也同樣不會執行接口的<clinit>()方法。

  5. 虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確地加鎖和同步,若是多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>()方法,其餘線程都須要阻塞等待,直到活動線程執行<clinit>()方法完畢。若是在一個類的<clinit>()方法中有耗時很長的操做,那就可能形成多個線程阻塞,在實際應用中這種阻塞每每是很隱蔽的。

下面給出一個簡單的例子,以便更清晰地說明如上規則:

class Father{
	public static int a = 1;
	static{
		a = 2;
	}
}
 
class Child extends Father{
	public static int b = a;
}
 
public class ClinitTest{
	public static void main(String[] args){
		System.out.println(Child.b);
	}
}
複製代碼

輸出結果:

2
複製代碼

看下運行代碼後的步驟:

  1. 準備階段:爲類變量分配內存並設置類變量初始值,這樣a和b均被賦值爲默認值0
  2. 初始化階段:然後再在調用<clinit>()方法時給他們賦予程序中指定的值
    咱們調用Child.b時,觸發Child的<clinit>()方法,根據規則2,在此以前,要先執行完其父類Father的<clinit>()方法,
    又根據規則1,在執行<clinit>()方法時,須要按static語句或static變量賦值操做等在代碼中出現的順序來執行相關的
    static語句,所以當觸發執行Father的<clinit>()方法時,會先將a賦值爲1,再執行static語句塊中語句,將a賦值爲2,
    然後再執行Child類的<clinit>()方法,這樣便會將b的賦值爲2.
    
    若是咱們顛倒一下Father類中「public static int a = 1;」語句和「static語句塊」的順序,程序執行後,則會打印出1。
    很明顯是根據規則1,執行Father的<clinit>()方法時,根據順序先執行了static語句塊中的內容,
    後執行了「public static int a = 1;」語句。
    另外,在顛倒兩者的順序以後,若是在static語句塊中對a進行訪問(好比將a賦給某個變量),
    在編譯時將會報錯,由於根據規則1,它只能對a進行賦值,而不能訪問。
    
    複製代碼

6、使用

使用階段包括主動引用和被動引用,主動引用會引發類的初始化,而被動引用不會引發類的初始化。

主動引用

jvm有嚴格的規定,當且僅當如下五種狀況,也就是主動引用,纔會觸發類的初始化,除此以外,全部引用類的方法都不會觸發初始化!

  1. 遇到new,getstatic,putstatic,invokestatic這4條字節碼指令時,假如類還沒進行初始化, 則立刻對其進行初始化工做。 其實就是3種狀況:用new實例化一個類時、讀取或者設置類的靜態字段時(不包括被final修飾的靜態字段, 由於他們已經被塞進常量池了)、以及執行靜態方法的時候。

  2. 使用java.lang.reflect.*的方法對類進行反射調用的時候, 若是類尚未進行過初始化,立刻對其進行。

  3. 初始化一個類的時候,若是他的父親尚未被初始化,則先去初始化其父親。

  4. 當jvm啓動時,用戶須要指定一個要執行的主類(包含static void main(String[] args)的那個類), 則jvm會先去初始化這個類。

  5. 用Class.forName(String className);來加載類的時候,也會執行初始化動做。

    • 注意:ClassLoader的loadClass(String className);方法只會加載並編譯某類,並不會對其執行初始化。

被動引用

  • 引用父類的靜態字段,只會引發父類的初始化,而不會引發子類的初始化。
  • 定義類數組,不會引發類的初始化。
  • 引用類的static final常量,不會引發類的初始化(若是隻有static修飾,仍是會引發該類初始化的)。

被動引用的示例代碼:

class InitClass{
	static {
		System.out.println("初始化InitClass");
	}
	public static String a = null;
	public final static String b = "b";
	public static void method(){}
}
 
class SubInitClass extends InitClass{
	static {
		System.out.println("初始化SubInitClass");
	}
}
 
public class Test4 {
 
	public static void main(String[] args) throws Exception{
	// String a = SubInitClass.a;// 引用父類的靜態字段,只會引發父類初始化,而不會引發子類的初始化
	// String b = InitClass.b;// 使用類的final常量不會引發類的初始化
		SubInitClass[] sc = new SubInitClass[10];// 定義類數組不會引發類的初始化
	}
}
複製代碼

當使用階段完成以後,java類就進入了卸載階段。

7、卸載

在類使用完以後,若是知足下面的狀況,類就會被卸載:

  • 該類全部的實例都已經被回收,也就是java堆中不存在該類的任何實例。
  • 加載該類的ClassLoader已經被回收。
  • 該類對應的java.lang.Class對象沒有任何地方被引用,沒法在任何地方經過反射訪問該類的方法。

若是以上三個條件所有知足,jvm就會在方法區垃圾回收的時候對類進行卸載,類的卸載過程其實就是在方法區中清空類信息,java類的整個生命週期就結束了。

總結

整個類加載過程當中,除了在加載階段用戶應用程序能夠自定義類加載器參與以外,其他全部的動做徹底由虛擬機主導和控制。到了初始化纔開始執行類中定義的Java程序代碼(亦及字節碼),但這裏的執行代碼只是個開端,它僅限於<clinit>()方法。類加載過程當中主要是將Class文件(準確地講,應該是類的二進制字節流)加載到虛擬機內存中,真正執行字節碼的操做,在加載完成後才真正開始。

參考資料

【深刻Java虛擬機】之四:類加載機制

JVM之類加載機制

相關文章
相關標籤/搜索