一起學(xué)Java虛擬機(四):類加載機制

一起學(xué)Java虛擬機系列

前言

了解JVM是對Java程序員的基本要求,但是有多少同學(xué)和我有一樣醉心解bug堆布局,忘記了內(nèi)功修煉,對JVM的理解是零碎的。系統(tǒng)地學(xué)習(xí)一次JVM也許能讓我們在這條路走得更好更遠(yuǎn)。

類的生命周期

image.png

一個類型從被加載到虛擬機內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個生命周期將會經(jīng)歷加載(Loading)、驗證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)七個階段,其中驗證、準(zhǔn)備、解析三個部分統(tǒng)稱為連接(Linking)。

類加載的過程

加載

“加載”(Loading)階段是整個“類加載”(Class Loading)過程中的一個階段,在加載階段,Java虛擬機需要完成以下三件事情:

  1. 通過一個類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流。
  2. 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)。
  3. 在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口
    加載階段結(jié)束后,Java虛擬機外部的二進(jìn)制字節(jié)流就按照虛擬機所設(shè)定的格式存儲在方法區(qū)之中
    了,方法區(qū)中的數(shù)據(jù)存儲格式完全由虛擬機實現(xiàn)自行定義,《Java虛擬機規(guī)范》未規(guī)定此區(qū)域的具體
    數(shù)據(jù)結(jié)構(gòu)。類型數(shù)據(jù)妥善安置在方法區(qū)之后,會在Java堆內(nèi)存中實例化一個java.lang.Class類的對象,
    這個對象將作為程序訪問方法區(qū)中的類型數(shù)據(jù)的外部接口

驗證

驗證是連接階段的第一步,這一階段的目的是確保Class文件的字節(jié)流中包含的信息符合《Java虛擬機規(guī)范》的全部約束要求,保證這些信息被當(dāng)作代碼運行后不會危害虛擬機自身的安全。2011年《Java虛擬機規(guī)范(Java SE 7版)》出版,規(guī)范中大幅增加了驗證過程的描述,驗證階段大致上會完成下面四個階段的檢驗動作:文件格式驗證、元數(shù)據(jù)驗證、字節(jié)碼驗證和符號引用驗證。

1. 文件格式驗證

第一階段要驗證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機處理。

2. 元數(shù)據(jù)驗證

第二階段的主要目的是對類的元數(shù)據(jù)信息進(jìn)行語義校驗,保證不存在與《Java語言規(guī)范》定義相悖的元數(shù)據(jù)信息。

3. 字節(jié)碼驗證

第三階段是整個驗證過程中最復(fù)雜的一個階段,主要目的是通過數(shù)據(jù)流分析和控制流分析,確定程序語義是合法的、符合邏輯的。在第二階段對元數(shù)據(jù)信息中的數(shù)據(jù)類型校驗完畢以后,這階段就要對類的方法體(Class文件中的Code屬性)進(jìn)行校驗分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的行為。

4. 符號引用驗證

最后一個階段的校驗行為發(fā)生在虛擬機將符號引用轉(zhuǎn)化為直接引用[3]的時候,這個轉(zhuǎn)化動作將在連接的第三階段——解析階段中發(fā)生。符號引用驗證可以看作是對類自身以外(常量池中的各種符號引用)的各類信息進(jìn)行匹配性校驗,通俗來說就是,該類是否缺少或者被禁止訪問它依賴的某些外部類、方法、字段等資源。

準(zhǔn)備

準(zhǔn)備階段是正式為類中定義的變量(即靜態(tài)變量,被static修飾的變量)分配內(nèi)存并設(shè)置類變量初始值的階段。
關(guān)于準(zhǔn)備階段,還有兩個容易產(chǎn)生混淆的概念筆者需要著重強調(diào),首先是實現(xiàn)這時候進(jìn)行內(nèi)存分配的僅包括類變量,而不包括實例變量,實例變量將會在對象實例化時隨著對象一起分配在Java堆中。其次是這里所說的初始值“通常情況”下是數(shù)據(jù)類型的零值。

public static int value = 123;

例如value在準(zhǔn)備階段過后的初始值為0,而不是123。而把value賦值為123的putstatic指令是程序被編譯后,存放于類構(gòu)造器<clinit>()方法之中,所以把value賦值為123的動作要到類的初始化階段才會被執(zhí)行。

image.png

public static final int value = 123;

對于ConstantValue屬性,那在準(zhǔn)備階段變量值就會被初始化為ConstantValue屬性所指定的初始值。

解析

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

1. 符號引用(Symbolic References):

符號引用以一組符號來描述所引用的目標(biāo),符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標(biāo)即可。符號引用與虛擬機實現(xiàn)的內(nèi)存布局無關(guān),引用的目標(biāo)并不一定是已經(jīng)加載到虛擬機內(nèi)存當(dāng)中的內(nèi)容。各種虛擬機實現(xiàn)的內(nèi)存布局可以各不相同,但是它們能接受的符號引用必須都是一致的,因為實現(xiàn)符號引用的字面量形式明確定義在《Java虛擬機規(guī)范》的Class文件格式中。

2. 直接引用(Direct References):

直接引用是可以直接指向目標(biāo)的指針、相對偏移量或者是一個能間接定位到目標(biāo)的句柄。直接引用是和虛擬機實現(xiàn)的內(nèi)存布局直接相關(guān)的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標(biāo)必定已經(jīng)在虛擬機的內(nèi)存中存在。

初始化

類的初始化階段是類加載過程的最后一個步驟,之前介紹的幾個類加載的動作里,除了在加載階段用戶應(yīng)用程序可以通過自定義類加載器的方式局部參與外,其余動作都完全由Java虛擬機來主導(dǎo)控制。直到初始化階段,Java虛擬機才真正開始執(zhí)行類中編寫的Java程序代碼,將主導(dǎo)權(quán)移交給應(yīng)用程序。

進(jìn)行準(zhǔn)備階段時,變量已經(jīng)賦過一次系統(tǒng)要求的初始零值,而在初始化階段,則會根據(jù)程序員通過程序編碼制定的主觀計劃去初始化類變量和其他資源。我們也可以從另外一種更直接的形式來表達(dá):初始化階段就是執(zhí)行類構(gòu)造器<clinit>()方法的過程。<clinit>()并不是程序員在Java代碼中直接編寫的方法,它是Javac編譯器的自動生成物。

  • <clinit>()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊(static{}塊)中的語句合并產(chǎn)生的,編譯器收集的順序是由語句在源文件中出現(xiàn)的順序決定的,靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的變量,定義在它之后的變量,在前面的靜態(tài)語句塊可以賦值,但是不能訪問.
public class Test {
 static {實現(xiàn)
 i = 0; // 給變量復(fù)制可以正常編譯通過
 System.out.print(i); // 這句編譯器會提示“非法向前引用”
 }
 static int i = 1;
}
  • <clinit>()方法與類的構(gòu)造函數(shù)(即在虛擬機視角中的實例構(gòu)造器<init>()方法)不同,它不需要顯式地調(diào)用父類構(gòu)造器,Java虛擬機會保證在子類的<clinit>()方法執(zhí)行前,父類的<clinit>()方法已經(jīng)執(zhí)行完畢。因此在Java虛擬機中第一個被執(zhí)行的<clinit>()方法的類型肯定是java.lang.Object。
  • 由于父類的<clinit>()方法先執(zhí)行,也就意味著父類中定義的靜態(tài)語句塊要優(yōu)先于子類的變量賦值操作。
static class Parent {
 public static int A = 1;
 static {
 A = 2;
 }
}
static class Sub extends Parent {
 public static int B = A;
}
public static void main(String[] args) {
    System.out.println(Sub.B);//輸出 2
}
  • <clinit>()方法對于類或接口來說并不是必需的,如果一個類中沒有靜態(tài)語句塊,也沒有對變量的賦值操作,那么編譯器可以不為這個類生成<clinit>()方法。
  • 接口中不能使用靜態(tài)語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生成<clinit>()方法。但接口與類不同的是,執(zhí)行接口的<clinit>()方法不需要先執(zhí)行父接口的<clinit>()方法,因為只有當(dāng)父接口中定義的變量被使用時,父接口才會被初始化。此外,接口的實現(xiàn)類在初始化時也一樣不會執(zhí)行接口的<clinit>()方法。
  • Java虛擬機必須保證一個類的<clinit>()方法在多線程環(huán)境中被正確地加鎖同步,如果多個線程同時去初始化一個類,那么只會有其中一個線程去執(zhí)行這個類的<clinit>()方法,其他線程都需要阻塞等待,直到活動線程執(zhí)行完畢<clinit>()方法。如果在一個類的<clinit>()方法中有耗時很長的操作,那就可能造成多個進(jìn)程阻塞,在實際應(yīng)用中這種阻塞往往是很隱蔽的
static class DeadLoopClass {
    static {
        // 如果不加上這個if語句,編譯器將提示“Initializer does not complete normally”并拒絕編譯
        if (true) {
            System.out.println(Thread.currentThread() + "init DeadLoopClass");
            while (true) {
            }
        }
    }
   }

   public static void main(String[] args) {
    Runnable script = new Runnable() {
    public void run() {
        System.out.println(Thread.currentThread() + "start");
        DeadLoopClass dlc = new DeadLoopClass();
        System.out.println(Thread.currentThread() + " run over");
    }};
    Thread thread1 = new Thread(script);
    Thread thread2 = new Thread(script);
    thread1.start();
    thread2.start();
   }

類加載器

比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源于同一個Class文件,被同一個Java虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等。

雙親委派模型

image.png

1. 啟動類加載器(Bootstrap Class Loader):

這個類加載器負(fù)責(zé)加載存放在
<JAVA_HOME>\lib目錄,或者被-Xbootclasspath參數(shù)所指定的路徑中存放的,而且是Java虛擬機能夠
識別的(按照文件名識別,如rt.jar、tools.jar,名字不符合的類庫即使放在lib目錄中也不會被加載)類
庫加載到虛擬機的內(nèi)存中。

2. 擴展類加載器(Extension Class Loader):

這個類加載器是在類sun.misc.Launcher$ExtClassLoader中以Java代碼的形式實現(xiàn)的。它負(fù)責(zé)加載<JAVA_HOME>\lib\ext目錄中,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中所有的類庫。

3. 應(yīng)用程序類加載器(Application Class Loader):

由于應(yīng)用程序類加載器是ClassLoader類中的getSystem?ClassLoader()方法的返回值,所以有些場合中也稱它為“系統(tǒng)類加載器”。它負(fù)責(zé)加載用戶類路徑(ClassPath)上所有的類庫,開發(fā)者同樣可以直接在代碼中使用這個類加載器。如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認(rèn)的類加載器

雙親委派模型的工作過程

如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應(yīng)該傳送到最頂層的啟動類加載器中,只有當(dāng)父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去完成加載。

雙親委派模型的實現(xiàn)

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
 // 首先,檢查請求的類是否已經(jīng)被加載過了
 Class c = findLoadedClass(name);
 if (c == null) {
 try {
 if (parent != null) {
 c = parent.loadClass(name, false);
 } else {
 c = findBootstrapClassOrNull(name);
 }
 } catch (ClassNotFoundException e) {
 // 如果父類加載器拋出ClassNotFoundException
 // 說明父類加載器無法完成加載請求
 }
 if (c == null) {
 // 在父類加載器無法加載時
 // 再調(diào)用本身的findClass方法來進(jìn)行類加載
 c = findClass(name);
 }
 }
 if (resolve) {
 resolveClass(c);
 }
 return c;
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容