類加載子系統

Class Load System

加載:把二進制字節碼流(可以是class文件、網絡字節流)加載到內存中,這片內存稱爲方法區(存放Code 字節碼 + 元數據 (類、字段(名稱和描述符)、方法(名稱和描述符)、常量池信息),然後經過以下幾個階段來完善class字節碼中信息,爲虛擬機的執行做準備。

因此在加載階段虛擬機需要完成的三件事

1、通過全限定名獲取二進制字節流(可以從類文件、ZIP(jar、war)、網絡、動態生成類(代理原理)、數據庫、有其他文件生成(JSP生成對應class類))

2、將字節碼中的靜態存儲結構轉化成方法區運行時數據結構(元數據區+代碼區)

3、生成java.lang.Class對象,作爲方法區這個類的各種數據訪問入口

以下階段包括:

加載–>連接–>初始化

加載階段可能會在連接階段進行觸發,類加載的各個階段是交替開始的。

首先jvm會通過類加載器加載Main class數據到內存中,然後進行連接階段,驗證A class文件是否合法在此階段進而會load SubClass和SuperClass進入內存中,檢查字段a是否允許被訪問。進而進入了Main的準備階段,給static變量分配內存並賦予初始值,然後進入解析階段,會將符號變量轉換成直接引用(會到SubClass根據字段描述符號找對應字段,如果沒找到則去父類SuperClass找,然後替換成邏輯地址),最後執行Main由編譯器生成的方法初始化其值,然後初始化其SuperClass類(執行SuperClass的方法)

package com.dynamo

 class Main {

  public static void main(){
    
     SubClass.a=88;
  
  }

}


 public class SubClass extends SuperClass {

    static {
        System.out.println("sub class init");
    }

    public static void main(String[] args) {
         SubClass.a = 88;
    }
}


 public class SuperClass {
    protected static int a = 1;
    static {
        System.out.println("super class init");
    }
}

java Main 輸出結果爲:

main init
super class init

由於類變量a是在SuperClass類中,因而只初始化SuperClass類,但是SubClass也需要被加載到內存中(因爲在Main class中需要檢查是否有權限訪問類變量a),如下圖爲load class過程圖,類似於樹的廣度優先遍歷算法

如圖 load_step.png

在這裏插入圖片描述

Load

假設1:如果我們在程序裏面自定義命名java.lang.Object 這個類,這個類會被加載到內存裏嗎?

假設如果會,那麼如果Object代碼是一個不安全(損壞硬件)的代碼,則被加載到內存中,那麼安全就不能保證。 那jvm如何做到只會加載自己rt.jar下的Object類呢?

###雙親委派模式:

有四類類加載器(搜索類範圍不一樣):

Bootstrap ClassLoader(C++實現的) : 只加載java定義api core類 /java_home/lib/rt.jar裏的類集合

Extension ClassLoader : 只加載 /java_home/lib/ext 裏面的類集合

App ClassLoader : 只加載用戶寫的java class,也就是class path下的所有類

繼承ClassLoader 自定義類加載器 : 可以隨意指定搜索範圍,可以是硬盤裏某個文件,也可以來自網絡字節流

拿上面的舉例子:加載自定義類 java.lang.Object

沒有自己重寫類加載器時,默認交給App ClassLoader

1、首先通過類全限定名到App ClassLoader查找是否已經被加載過,若加載了則返回Class對象則結束,否則到2

2、再委派給父類加載器 Extension ClassLoader 判斷該類是否已經被加載了,若加載則返回Class對象則結束,否則到3

3、委派給父類加載器 Bootstrap ClassLoader 斷該類是否已經被加載了,若加載則返回Class對象則結束,否則到4

4、根據類全限定名,到rt.jar搜索該類,如果存在則會加載該類到內存裏,否則又會向下進行委派給Extension ClassLoader,同樣如果在 ext文件下仍然搜不到則委派給App ClassLoader,最後如果載此類加載器找不到的話,直接拋出異常 ClassNotFoundException ClassNotDefError

所以從上面的過程可以看出,自定義的Object類並沒有加載,而是加載了rt.jar裏的Object類,由於使用委派模式保證了一個同名的類在內存裏只會被加載一次,並僅存在一個Class對象。

假設2 如果自定一個類加載器,不走委派模式,指定加載自定義的java.lang.Object到內存裏,是否可行?

不可以,會拋出一個異常 java.lang.SecurityException:Prohinited package name:java.lang,因爲jvm控制了不能加載自定義的java.lang 這樣命名包的類,有興趣的可以動手試試

加載類過程的核心方法loadClass:

loadClass是個線程安全方法,當多個線程同時加載某個類時,只允許一個線程進行加載類操作,當該線程結束時其他線程直接獲取加載到內存的Class對象

public synchronized Class<?> loadClass(String name,boolean resolve) throws ClassNotFoundException{

//首先檢查請求的類是否被加載過
Class c = findLoadedClass(name);

if(c == null){
	try{
	   
	   if(parent != null ){
	     //委派給父類加載器進行加載
	     c = parent.loadClass(name,false);
	   
	   }else{
	   	 //如果parent爲null,表示當前時Bootstrap ClassLoader
	     c = findBootstrapClassOrNull(name);
	   }
	   
	}catch(ClassNotFoundException e){
		//說明父類在其搜索範圍沒有發現該類,則委派給其子類進行加載
	}
	
	if(c == null){
		//在父類加載器無法加載的時候再將調用本身的findClass方法進行加載
	   c = findClass(name);
	
	}

  if(resolve){
  	
  	resolveClass(c);
  
  }
}


return c;

}

在自定義類加載器時,繼承ClassLoader類,然後重寫loadClass類,或findClass,此時建議重寫findClass方法因爲,loadClass方法具有委派模式邏輯,也是爲了安全、避免多個類出現在內存中。
如果重寫了loadClass方法邏輯,那麼就不滿足了委派模式了。

類全限定名+Class Loader 唯一確定一個類,同個類被不同的類加載器加載,那麼也屬於不同的類對象,兩個類必定不相等。這裏的相等包含Class中的equals方法、isAssignableFrom()方法isInstance()、instanceof

加載類的過程如圖:class-loader.png

在這裏插入圖片描述

Link

對於C++ 直接將源程序編譯成機器碼,而java只會編譯成平臺無關的class二進制流,從而可以看出,如果換一個操作系統執行c++代碼都要重新編譯,而java就不需要。

連接:
簡單的理解爲將我們寫的代碼中的符號引用轉化成邏輯地址引用(例如在代碼中類A 中的方法test中調用了類B的 f1方法,這個時候在類A中僅僅使用 B.f1()符號來調用,然後通過連接,找到類B的方法f1的邏輯地址,然後將原來符號替換成地址)

這裏的邏輯地址:有人可能會問爲什麼不直接的內存物理地址呢?原因是這個時候該程序還沒有被操作系統載入,因此實際的地址肯定不知道。當程序載入系統中後,操作系統會保持邏輯地址和物理地址的對應關係,所以通過訪問邏輯地址就可以獲取到真實內存裏的數據(內存地址映射)。

其中C++是在編譯的時候完成連接操作,而java是在load class文件時將符號引用轉化成能直接定位到目標的直接引用,對於多態的實現就是在運行進行解析的,當執行指令的時候進行查找到真實類型的方法地址引用,然後進行替換的。

Verfication 字節碼驗證

在加載階段之後進行連接階段的第一個步,在java語言中 對於我們訪問不存在的常量,以及跳到不存在的代碼處,無法訪問數組邊界以外的事情,因爲不符合正常語法,編譯器會提示。 但是我們知道字節碼不一定由java語言編譯生成的,還可以自己通過編譯進行編碼,這個時候就可以實現java代碼無法做到的事情了。因此字節碼驗證對系統穩定性起到了決定性作用。

會由以下四個步驟組成:

1、類文件格式驗證

文件是否以0xCAFEBABE 開頭,是否定義了不存在的常量池類型,常量池索引值是否指向了不存在的常量或不符號類型的常量。

該步驟,基於類的二進制流進行,只有類文件格式驗證通過後纔會在方法區生成一個Class對象,那下面3個步驟都是基於方法區的存儲結構進行的,不會操作字節流。

2、元數據驗證

對字節碼描述消息進行語義分析,例如除類java.lang.Object是否有父類,是否繼承了final 修飾的類,是否實現了抽象類或接口中所要求實現的方法

3、字節碼驗證

主要對類的方法體進行校驗分析,保證類的方法在運行時不會做出危害虛擬機安全事件。

例如保證操作數棧中的數據類型與操作碼配合工作,例如操作數棧方了int類型,使用時確按照long類型存入本地變量表。

保證跳轉指令不能跳到方法體以外字節碼上

保證方法體的類型轉換有效,例如父類轉換成子類,或轉換成一個毫無繼承關係的數據類型

字節碼的校驗也不能保證百分百準確,因爲通過程序去檢驗程序邏輯性是否無法做到絕對準確。

4、符號引用驗證

該校驗動作發生在第三階段解析中

確保引用的類可以根據全限定名能找到對應的類

在引用類中是否存在符合方法描述符和字段的描述符以及簡單字段名和方法名

符號引用的類、字段、方法的訪問性是否可被當前類訪問

Preparation 準備(給static字段分配初始值)

只會給類變量也就是static修飾的字段,所需要的內存在方法區進行分配。 而對象實例變量會在對象初始化 new 時候在堆內分配內存。

static int value = 123 在準備階段會給value變量分配一個初始值爲0,而把value值賦值爲123的putstatic指令是程序編譯後存放於類構造器方法,所以這個階段是在初始化進行的。

特殊例子: static final int value = 123 會在編譯的時候在field表中生存ConstantValue屬性,在準備階段會將value賦值爲123

Resolution 解析(符號引用換成直接引用)

一般都是在類加載的時候完成解析,但是爲了支持java語言的運行時綁定(動態綁定),某些情況下是在初始化階段之後纔開始的。 例如我們熟悉的多態的實現。

1、符號引用

通過常量池中的 Constant_class_info Constant_method_info Constant_field_info 這種唯一標識的符號來表示調用的對象。

例如方法體中有如下字節碼 invokespecail #1 #1表示class字節碼中第一個常量 Constant_method_info類型的常量(class_name_index,name_and_type_index) java/lang/Object.()V 表示的是類全名+方法描述符(方法名稱+方法參數以及返回值),含義就是調用父類Object中的構造方法進行初始化實例變量。

2、直接引用

直接引用就是和內存佈局有關,通過這個直接引用就可以定位到內存中對應的變量和方法入口地址,此處爲邏輯地址,操作系統中的地址變化器會幫我們把邏輯地址和物理地址映射起來。

3、解析過程

類或接口的解析:例如new #1 這個指令就會把類符號引用替換成直接引用,這時會經過類加載器加載到內存中,在這個過程中又會觸發其他相關類的加載動作,例如父類或實現的接口。最終該類對應內存中的邏輯地址就是直接引用

類字段解析、 類方法解析、接口方法解析:

首先會拿到類全限定名通過類加載器進行加載到內存中,若已經被加載了則進行後面的驗證操作,然後再拿方法名稱和描述符號查找對應方法入口地址,如果沒找到則繼續從父類進行搜索。

這裏有個優化,在每個類中都有個虛擬方法表,存儲的就是(方法名+描述符)和入口地址映射關係(如何重寫的方法那麼入口地址就是子類中的方法入口地址,否則存放的就是父類中的方法入口地址),這樣在後面動態解析過程中,就沒必要每調用一次方法都進行一次搜索方法過程,直接從該類的虛擬方法表獲取。

Initialization

初始化過程:由編譯器會給有類變量或有static{} 靜態塊類生成一個 類初始化方法,靜態變量和靜態塊裏初始化都會統一放在此方法進行,按照代碼順序分別進行初始化類變量。實例變量初始化需要顯示通過調用父類構造器,但是類初始化會默認先初始化完父類類變量。

接口雖然沒有static{} 但是也有類變量,因此編譯器也會自動生成方法,然而接口A的初始化並不會導致其父接口B的初始化,只有當B接口定義的變量使用時,父接口B纔會進行初始化(加載、連接、初始化類變量)

同樣類A進行初始化時並不會初始化實現的接口A

方法加鎖保證了線程安全,當多個線程同時初始化類A(加載、連接、初始化),有且只有一個線程進行這個過程,其他線程一直在等待,直到初始化完成了喚醒其他線程之後也不會進入方法,同樣loadClass方法也是synchronized 同步的。一個類在同個類加載器下只會進行一次初始化

類加載時機

jvm規範中固定有且只有以下5種情況會立即進行初始化(加載、驗證、準備需要在此之前)

1、遇到 new、putstatic、getstatic、invokestatic 這4條指令如果類沒有進行過初始化,則需要進行初始化,new實例化對象、讀取或設置一個靜態字段(被fianl修飾,在編譯階段放入常量池的靜態字段除外)的時候,以及調用一個類靜態方法時。

2、使用java.lang.reflect包方法對類進行反射調用時候

3、在初始化類時候,如果發現父類沒有初始化,則先初始化父類

4、虛擬機啓動時需要用指定一個包含main方法的主類,則會先初始化該主類

5、在jdk1.7的動態語言時,如果MethodHandle解析的結果REF_getStatic 並且此方法句柄對應類沒有初始化,那麼先進行初始化

以上5種情況爲類主動引用,除此之外所有引用的類都不會初始化,稱爲被動引用,被動引用的意思就是:當初始化A類時候,由於A類引用了B類,那麼A爲主動引用 B就是被動引用,並不會觸發初始化但是會進行其他階段,例如加載、驗證、準備等階段。

以下爲三種被動引用的情況:

1、當訪問的是父類的靜態字段時 例如SubClass.a,只會初始化父類SuperClass

2、SupClass[] s = new SuperClass[0] 由虛擬機使用newarray指令創建的[Ltest.SuperClass,此類繼承於Object,此時並不會初始化SuperClass

3、當類A訪問類B的 static final String s = “test” 修飾的字段時,在編譯的時候通過常量傳播優化,將對應的常量值存儲到類A的常量池中,類A和類B在編譯之後不存在任何聯繫。在訪問 B.s時候並不會加載B類,這也減少了類的加載次數,加快了運行速度

接口和類在加載過程中有什麼區別:

接口並沒有static{} 靜態塊,但是編譯器仍然會生成 方法對接口變量進行初始化,其中在需要初始化場景第三種,接口的初始化不需要先初始化父接口,而是使用到了父接口再去初始化。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章