深入拆解類加載器,這樣的姿勢(shì)你還不懂嗎?

本文導(dǎo)讀:

1、前奏,舉個(gè)生活中的小栗子

2、為何Java類型加載、連接在程序運(yùn)行期完成?

3、一個(gè)類在什么情況下才會(huì)被加載到JVM中?

什么是主動(dòng)使用、被動(dòng)使用?代碼示例助你透徹理解類初始化的時(shí)機(jī)。

4、類的加載(Loading)內(nèi)幕透徹剖析

類加載做的那些事兒、雙親委派模型工作過程、ClassLoader源碼解析

5、Tomcat如何打破雙親委派模型的

6、上下文類加載器深入淺出剖析

7、最后總結(jié)

1、前奏,舉個(gè)生活中的小栗子

春節(jié)馬上要到了,大家是不是都在迫不及待的等著回家團(tuán)圓了呢?

大春運(yùn)早已啟動(dòng),回家的過程其實(shí)是個(gè)「辛苦活」,有的同學(xué)還沒有買到票呢,蒙眼狂奔終于搶到了,發(fā)現(xiàn)竟然是個(gè)站票~,退了,連站票的機(jī)會(huì)都沒了吧?

昨天還聽一位同學(xué)說:『嘿嘿,去年我提前就買到票了,但是... 但是... 去錯(cuò)火車站了。。。尼瑪,當(dāng)時(shí)那是啥心情啊~ 幸運(yùn)的是后來又刷到票了,不然就真回不去了!』

心塞塞

回家大部分朋友都要乘坐交通工具,不管你乘坐什么樣的交通工具出行,對(duì)于「交通管理」內(nèi)部來說,最最重要的任務(wù)就是保障大家得出行安全。

交通出行

那么如何保障大家的出行安全呢?

乘坐地鐵、飛機(jī)等這些公共交通工具,必不可少的最重要的環(huán)節(jié)就是『安檢』,不是什么東西都可以隨便讓你帶的,都是有明文規(guī)定的,比如易燃易爆、酒類等都是有限制的。

交通出行的大體過程,有點(diǎn)類似類文件加載到Java虛擬機(jī)(簡(jiǎn)稱 JVM)的過程,程序中運(yùn)行的各種類文件(比如Java、Kotlin),也是要必須經(jīng)過『安檢』的,才能允許進(jìn)入到JVM中的,一切都是為了安全。

當(dāng)然,安檢的標(biāo)準(zhǔn)是不同的。

接下來,我們進(jìn)入正題,一起來看看類文件是如何被加載到JVM當(dāng)中的。

類加載過程

上圖的對(duì)比只是為了方便理解 ,抽象出來一層『安全檢查』,其實(shí)就是『類加載』的過程。
這個(gè)過程JVM當(dāng)中約束了規(guī)范和標(biāo)準(zhǔn),都會(huì)經(jīng)過加載、驗(yàn)證、準(zhǔn)備、解析、初始化五個(gè)階段。

這里一定要說一個(gè)概念,個(gè)人認(rèn)為對(duì)于理解類加載過程挺重要的。

更準(zhǔn)確的說法,應(yīng)該是類型的加載過程,在Java代碼中,類型的加載、連接、初始化都是在程序運(yùn)行時(shí)完成的。

這里的類型,是指你在開發(fā)代碼時(shí)常見的class、interface、enum這些關(guān)鍵字的定義,并不是指具體的class對(duì)象。

舉個(gè)??:

Object obj = new Object();

new出來的obj是Object類型嗎?當(dāng)然不是,obj只是通過new創(chuàng)建出來的Object對(duì)象,而類型實(shí)際是Object類本身。而要想創(chuàng)建Object對(duì)象的前提,必須要有類型的信息,才能在Java堆中創(chuàng)建出來。所以,這里要明確區(qū)分開。

絕大多數(shù)情況下,類型是提前編寫好的,比如Object類是由JDK已經(jīng)提供的。另外一些情況是可以在運(yùn)行期間動(dòng)態(tài)的生成出來,比如動(dòng)態(tài)代理(程序運(yùn)行期完成的)。

2、為何Java類型加載、連接在程序運(yùn)行期完成?

其實(shí),運(yùn)行區(qū)間能做這件事,就為一些有創(chuàng)意的開發(fā)人員提供了很多的可能性。一切的文件都已經(jīng)存在,程序運(yùn)行的過程中可以采取一些特殊的處理方式把這些之前已經(jīng)存在或者運(yùn)行期生成出來的這些類型有機(jī)的裝配在一起。

Java本身是一門靜態(tài)的語言,而他的很多特性又具有動(dòng)態(tài)語言才能擁有的特質(zhì),也因此類型的加載、連接和初始化在運(yùn)行期間完成起到了很大的幫助作用。

類型的加載:查找并加載類的二進(jìn)制數(shù)據(jù)(字節(jié)碼文件),最常見的,是將類的Class文件從磁盤加載到內(nèi)存中。

類型的連接:將類與類的關(guān)系確定好,對(duì)于字節(jié)碼相關(guān)的處理、驗(yàn)證、校驗(yàn)在加載連接階段去完成的。字節(jié)碼本身可以被人為操縱的,也因此可能有惡意的可能性,所以需要校驗(yàn)。

  • 驗(yàn)證:確保被加載類的正確性,就是要按照J(rèn)VM規(guī)范定義的。

  • 準(zhǔn)備:為類的靜態(tài)變量分配內(nèi)存,并將其初始化為默認(rèn)值

class Test {
         public static int num = 1; 
}

上述代碼示例中的中間過程,在將類型加載到內(nèi)存過程中,num分配內(nèi)存,首先設(shè)置為0,1是在后續(xù)的初始化階段賦值給num變量。

  • 解析:把類中的符號(hào)引用轉(zhuǎn)換為直接引用

符號(hào)引用: 間接的引用方式,通過一個(gè)符號(hào)的表示一個(gè)類引用了另外的類。 直接引用:直接引用到目標(biāo)對(duì)象中的內(nèi)存的位置
初始化階段:為類的靜態(tài)變量賦予正確的初始值。

類型的初始化:比如一些靜態(tài)的變量的賦值是在初始化階段完成的。

3、一個(gè)類在什么情況下才會(huì)被加載到JVM中?

Java程序?qū)︻惖氖褂梅绞娇煞譃閮煞N:

  • 主動(dòng)使用

  • 被動(dòng)使用

特別的重要:

所有的Java虛擬機(jī)實(shí)現(xiàn)必須在每個(gè)類或接口被java程序首次主動(dòng)使用時(shí)才初始化他們。

主動(dòng)使用八種情況):

1)創(chuàng)建類的實(shí)例,比如new一個(gè)對(duì)象

2)訪問某一個(gè)類或接口的靜態(tài)變量,或者對(duì)該靜態(tài)變量賦值 (訪問類的靜態(tài)變量的助記符getstatic,賦值是putstatic)。

3)調(diào)用類的靜態(tài)方法 (應(yīng)用invokestatic助記符)。

4)使用java.lang.reflect包的方法對(duì)類型進(jìn)行反射調(diào)用,比如:Class.forName(“com.test.Test") 通過反射的方式獲取類的Class對(duì)象。

5)初始化一個(gè)類的子類,比如有class Parent{}、子類class Child extends Parent{},當(dāng)初始化Child類時(shí)也表示對(duì)Parent類的主動(dòng)使用,Parent類也要全部初始化。

6)Java虛擬機(jī)啟動(dòng)時(shí)被標(biāo)注為啟動(dòng)類的類,即有main方法的類。

7)JDK1.7開始提供的動(dòng)態(tài)語言支持:java.lang.invoke.MethodHandle實(shí)例的解析結(jié)果REF_getStatic, REF_putStatic, REF_invokeStatic句柄對(duì)應(yīng)的類沒有初始化,則初始化。

8)當(dāng)一個(gè)接口中定義了JDK 8新加入的默認(rèn)方法(被default關(guān)鍵字修飾的接口方法)時(shí),如果有這個(gè)接口的實(shí)現(xiàn)類發(fā)生了初始化,那該接口要在其之前被初始化。

除了上述所講的八種情況,其他使用Java類的方式都被看作是類的被動(dòng)使用,都不會(huì)導(dǎo)致類的初始化。

另外,要特別說明的一點(diǎn)

接口的加載過程與類加載過程會(huì)有所不同,接口不能使用 「static{}」語句塊,但是編譯器會(huì)為接口生成對(duì)應(yīng)的 <clinit>()類構(gòu)造器,用于初始化接口中所定義的成員變量。

主動(dòng)使用的第5種:當(dāng)子類初始化時(shí),要求其父類也要全部初始化完成。但是,對(duì)于一個(gè)接口的初始化時(shí),并不要求其父接口要全部初始化完成,只有在真正使用到父接口時(shí)(比如引用接口中定義的常量)時(shí)才會(huì)去初始化,有點(diǎn)延遲加載的意思。

被動(dòng)使用示例:

1)通過子類引用父類的靜態(tài)字段,不會(huì)導(dǎo)致子類的初始化

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

    public static int a = 123;
}
public class Child extends Parent {
    static {
        System.out.println("Child init...");
    }
}

// Test類打印,子類直接調(diào)用父類的靜態(tài)字段
public static void main(String[] args) {
        System.out.println(Child.a);
}

輸出結(jié)果:

Parent init....
123

根據(jù)輸出結(jié)果看到,不會(huì)輸出 Child init...,通過其子類來引用父類中定義的靜態(tài)字段,只會(huì)觸發(fā)父類的初始化而不會(huì)觸發(fā)子類的初始化,對(duì)于靜態(tài)字段,只有直接定義這個(gè)字段的類才會(huì)被初始化。

2) 創(chuàng)建數(shù)組類對(duì)象,并不會(huì)導(dǎo)致引用的類初始化

public class Child extends Parent {
    static {
        System.out.println("Child init...");
    }
}

// 使用 Child 引用創(chuàng)建個(gè)數(shù)組
public static void main(String[] args) { 
        Child[] child = new Child[1];
        System.out.println(child);
    }

輸出結(jié)果:

[Lcom.dskj.jvm.beidong.Child;@7852e922

并沒有輸出Child init...證明并沒有初始化com.dskj.jvm.beidong.Child類,根據(jù)輸出結(jié)果看到了[Lcom.dskj.jvm.beidong.Child,帶了[L說明觸發(fā)了數(shù)組類的初始化階段,它是由JVM自動(dòng)生成的,繼承自java.lang.Object類,由于anewarray助記符觸發(fā)創(chuàng)建動(dòng)作的。

對(duì)于數(shù)組來說,JavaDoc通常將其所構(gòu)成的元素稱作為Component,實(shí)際上就是將數(shù)組降低一個(gè)維度的類型。

助記符:

anewarray:表示創(chuàng)建一個(gè)引用類型的(如類、接口、數(shù)組)數(shù)組,并將其引用值壓入棧頂。

newarray:表示創(chuàng)建一個(gè)指定的原始類型的(如int、float、char、short、double、boolean、byte)的數(shù)組,并將其引用值壓入棧頂。

對(duì)應(yīng)字節(jié)碼內(nèi)容:

對(duì)應(yīng)字節(jié)碼

3)調(diào)用ClassLoader的loadClass()方法,不會(huì)導(dǎo)致類的初始化。

代碼如下:

public class LoadClassTest {
    public static void main(String[] args) {
        try {
            ClassLoader.getSystemClassLoader().loadClass("com.dskj.jvm.passivemode.LoadClass");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

class LoadClass {
    public static final String STR = "Hello World";

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

沒有輸出 LoadClass init...,證明了調(diào)用系統(tǒng)類加載器的loadClass()方法,并不會(huì)初始化LoadClass類,因?yàn)镃lassLoader#loadClass()方法內(nèi)部傳入的resolve參數(shù)為false,表示Class不會(huì)進(jìn)入到連接階段,也就不會(huì)導(dǎo)致類的初始化。

public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
}

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
{
...
  if (resolve) {
          //** Links the specified class**
            resolveClass(c);
    }
}

4)final修飾的常量,編譯時(shí)會(huì)存入調(diào)用類常量池中,本質(zhì)上沒有引用到定義常量的類,不會(huì)導(dǎo)致類的初始化動(dòng)作。

看下面代碼:

public class ConstClassTest {
    public static void main(String[] args) {
        System.out.println(ConstClass.STR);
    }
}

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

    public static final String STR = "Hello World";
}

輸出結(jié)果:

Hello World

結(jié)果只會(huì)輸出 Hello World,不會(huì)輸出ConstClass init...,ConstClassTest類對(duì)常量ConstClass.STR的引用,實(shí)際被轉(zhuǎn)化為ConstClassTest類對(duì)自身常量池的引用了。也就是說,實(shí)際上ConstClassTest的Class文件之中并沒有ConstClass類的符號(hào)引用入口。

編譯完成,兩個(gè)ConstClassTest和ConstClass就沒有任何關(guān)系了。這句話如何能證明一下?

你可以先運(yùn)行一次,然后將編譯后的ConstClass.class文件從磁盤上刪除掉,再次運(yùn)行跟上面輸出結(jié)果是一樣的。

還不信?如下圖所示Idea中的運(yùn)行結(jié)果:

idea運(yùn)行

在IDEA下測(cè)試時(shí),如果你使用的Gradle來構(gòu)建,模擬上面的刪除class文件過程,要使用 xxx/out/production/ 目錄下生成編譯后的class文件,當(dāng)類沒有發(fā)生變化時(shí)不會(huì)重新生成class文件。如果使用默認(rèn)的 xxx/build/xx,每次運(yùn)行都會(huì)重新生成新的class文件。

如果有問題,可以在 Project Settings -> Modules -> 項(xiàng)目的 Paths 中調(diào)整編譯輸出目錄。

我們繼續(xù)在這個(gè)示例基礎(chǔ)上做修改:

public class ConstClassTest {
    public static void main(String[] args) {
        System.out.println(ConstClass.STR);
    }
}

class ConstClass {
    // STR 定義的常量通過UUID生成一個(gè)隨機(jī)串
    public static final String STR = "Hello World" + UUID.randomUUID();

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

注意,這里 STR 常量通過UUID生成一個(gè)隨機(jī)串,編譯是通過的。

直接運(yùn)行,輸出結(jié)果:

ConstClass init...
Hello World:d26d7f1d-2d46-41cb-b5dc-2b7b3fe61e74

看到了ConstClass init...,說明ConstClass類被初始化了。

將ConstClass.class文件刪除后,再次運(yùn)行:

Exception in thread "main" java.lang.NoClassDefFoundError: com/dskj/jvm/passivemode/ConstClass
    at com.dskj.jvm.passivemode.ConstClassTest.main(ConstClassTest.java:7)
Caused by: java.lang.ClassNotFoundException: com.dskj.jvm.passivemode.ConstClass
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 1 more

大家看到了嗎?ConstClass.class文件被刪除后,再次運(yùn)行就發(fā)生了 java.lang.NoClassDefFoundError 異常了,為什么?正是因?yàn)?ConstClass 類里定義的STR常量并不是編譯器能夠確定的值,那么其值就不會(huì)被放到調(diào)用類的常量池中。

這個(gè)示例可以好好理解下,同時(shí)印證了該類的初始化時(shí)機(jī)中,主動(dòng)使用和被動(dòng)使用的場(chǎng)景。

大家記住一個(gè)類的8種主動(dòng)使用情況,都是在開發(fā)過程中常見的使用方式。另外,注意下被動(dòng)使用的幾種情況,結(jié)合上面的列舉的代碼示例透徹理解。

類加載全過程的每一個(gè)階段,結(jié)合前文給出的圖示,詳細(xì)展開。

4、類的加載(Loading)內(nèi)幕透徹剖析

前面提到的類文件,就是后綴文件為.class的二進(jìn)制文件。

JVM在加載階段主要完成如下三件事

1)通過一個(gè)類的全限定名,即包名+類名 來獲取定義此類的二進(jìn)制字節(jié)流。

2)將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。

3)JVM內(nèi)存中生成一個(gè)代表該類的java.lang.Class對(duì)象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問入口。

對(duì)于第一點(diǎn)來說,并沒有要求這個(gè)二進(jìn)制字節(jié)流,具體以什么樣的方式從Class文件中讀取。

通過下面一張圖來匯總一下:

文件來源

解釋下比較常見的Class文件讀取方式:

1)從ZIP包中讀取Class文件,流行的SpringBoot/SpringCoud框架基本都打成Jar包形式,內(nèi)嵌了Tomcat,俗稱Fat Jar,通過java -jar可以直接啟動(dòng),非常方便。

另外,還有一些項(xiàng)目仍然是使用War包形式,并且使用單獨(dú)使用Tomcat這類應(yīng)用容器來部署的。

2)運(yùn)行時(shí)生成的Class文件,應(yīng)用最多的就是動(dòng)態(tài)代理技術(shù)了,比如CGLIB、JDK動(dòng)態(tài)代理。

雙親委派模型

思考個(gè)問題,這些Class文件是由誰來加載的呢?

實(shí)現(xiàn)這個(gè)動(dòng)作的代碼正是類加載器來完成的,類加載器在類層次劃分、OSGi、程序熱部署、代碼加密等領(lǐng)域大放異彩,成為Java技術(shù)體系中一塊重要的基石。

對(duì)于任意一個(gè)類,如何確定在JVM當(dāng)中的唯一性?必須是由加載該類的類加載器和該類本身一起共同確立在JVM中的唯一性。

每一個(gè)類加載器,都擁有一個(gè)獨(dú)立的類名稱空間。通俗理解:比較兩個(gè)類是否『相等』,這兩個(gè)類只有在同一個(gè)類加載器加載的前提下才有意義。否則,即使這兩個(gè)類來源于同一個(gè)Class文件,被同一個(gè)JVM加載,只要加載它們的類加載器不同,那這兩個(gè)類就必定不相等。

類加載器之間是什么關(guān)系?

如下圖所示,三種加載器之間的層次關(guān)系被稱為類加載器的 『雙親委派模型(Parents Delegation Model)』。

類加載器

雙親委派模型要求除了頂層的啟動(dòng)類加載器外,其余的類加載器都應(yīng)有自己的父類加載器。不過這里類加載器之間的父子關(guān)系一般不是以繼承(Inheritance)的關(guān)系來實(shí)現(xiàn)的,而是通常使用組合(Composition)關(guān)系來復(fù)用父加載器的代碼。圖:

這里說個(gè)有意思的問題,不止一次在某些文章留言中看到糾結(jié):『為什么叫做雙親?』國外文章寫的 parent delegation model,這里的parent不是單親嗎??應(yīng)該翻譯為單親委派模型才對(duì),全互聯(lián)網(wǎng)都跟著錯(cuò)誤走。。。其實(shí)parent這個(gè)英文單詞翻譯過來也有雙親的意思,不需要做個(gè)『杠精』,沒啥意義哈。

雙親委派模型工作過程

結(jié)合類加載器的自底向上的委托關(guān)系總結(jié):

假設(shè)一個(gè)類處于ClassPath下,版本是JDK8,默認(rèn)使用應(yīng)用類加載器進(jìn)行加載。

1)當(dāng)應(yīng)用類加載器收到了類加載的請(qǐng)求,會(huì)把這個(gè)請(qǐng)求委派給它的父類(擴(kuò)展類)加載器去完成。

2)擴(kuò)展類加載器收到類加載的請(qǐng)求,會(huì)把這個(gè)請(qǐng)求委派給它的父類(引導(dǎo)類)加載器去完成。

3)引導(dǎo)類加載器收到類加載的請(qǐng)求,查找下自己的特定庫是否能加載該類,即在rt.jar、tools.jar...包中的類。發(fā)現(xiàn)不能呀!返回給擴(kuò)展類加載器結(jié)果。

4)擴(kuò)展類加載器收到返回結(jié)果,查找下自己的擴(kuò)展目錄下是否能加載該類,發(fā)現(xiàn)不能??!返回給應(yīng)用類加載器結(jié)果。

5)應(yīng)用類加載器收到結(jié)果,額!都沒有加載成功,那只能自己加載這個(gè)類了,發(fā)現(xiàn)在ClassPath中找到了,加載成功。

你對(duì)并發(fā)很感興趣,自己創(chuàng)建了個(gè)跟JDK一樣的全限定名類LongAdder, java.util.concurrent.atomic.LongAdder,然后程序啟動(dòng)交給類加載器去加載,能成功嗎?

當(dāng)然不能!這個(gè)LongAdder是 Doug Lea 大神寫的,貢獻(xiàn)到JDK并發(fā)包下的,并且被安排在rt.jar包中了,因此是由 Bootstrap ClassLoader 類加載器優(yōu)先加載的,別人誰寫同樣的類,那就是故意跟JDK作對(duì),是絕對(duì)不容許的。

即使你寫了同樣的類,編譯可以通過,但是永遠(yuǎn)不會(huì)被加載運(yùn)行,被JDK直接忽略掉。

ClassLoader源碼分析

雙親委派模型在JDK中內(nèi)部是如何實(shí)現(xiàn)的?

JDK中提供了一個(gè)抽象的類加載器 ClassLoader,其中提供了三個(gè)非常核心的方法。

public abstract class ClassLoader {

    //每個(gè)類加載器都有個(gè)父加載器
    private final ClassLoader parent;

    public Class<?> loadClass(String name) {

            //查找一下這個(gè)類是不是已經(jīng)加載過了
            Class<?> c = findLoadedClass(name);

            //如果沒有加載過
            if( c == null ){
                //先委托給父加載器去加載,注意這是個(gè)遞歸調(diào)用
                if (parent != null) {
                        c = parent.loadClass(name);
                }else {
                        // 如果父加載器為空,查找Bootstrap加載器是不是加載過了
                        c = findBootstrapClassOrNull(name);
                }
            }
            // 如果父加載器沒加載成功,調(diào)用自己的findClass去加載
            if (c == null) {
                    c = findClass(name);
            }

            return c;
    }

    protected Class<?> findClass(String name){
         //1. 根據(jù)傳入的類名name,到在特定目錄下去尋找類文件,把.class文件讀入內(nèi)存
                ...

         //2. 調(diào)用defineClass將字節(jié)數(shù)組轉(zhuǎn)成Class對(duì)象
         return defineClass(buf, off, len);
    }

    // 將字節(jié)碼數(shù)組解析成一個(gè)Class對(duì)象,用native方法實(shí)現(xiàn)
    protected final Class<?> defineClass(byte[] b, int off, int len){
         ...
    }
}

參見ClassLoader核心代碼注釋,提取和印證幾個(gè)關(guān)鍵信息:

1)JVM 的類加載器是分層次的,它們有父子關(guān)系,每個(gè)類加載器都有個(gè)父加載器,是parent字段。

2)loadClass() 方法是 public 修飾的,說明它才是對(duì)外提供服務(wù)的接口。根據(jù)源碼可看出這是一個(gè)遞歸調(diào)用,父子關(guān)系是一種組合關(guān)系,子加載器持有父加載器的引用,當(dāng)一個(gè)類加載器需要加載一個(gè) Java 類時(shí),會(huì)先委托父加載器去加載,然后父加載器在自己的加載路徑中搜索 Java 類,當(dāng)父加載器在自己的加載范圍內(nèi)找不到時(shí),才會(huì)交還給子加載器加載,這就是所謂的『雙親委托模型』。

3)**findClass() **方法的主要職責(zé)就是找到 .class 文件,可能來自磁盤或者網(wǎng)絡(luò),找到后把.class文件讀到內(nèi)存得到byte[]字節(jié)碼數(shù)組,然后調(diào)用 defineClass() 方法得到 Class 對(duì)象。

4)defineClass() 是個(gè)工具方法,它的職責(zé)是調(diào)用 native 方法把 Java 類的字節(jié)碼解析成一個(gè) Class 對(duì)象,所謂的 native 方法就是由 C 語言實(shí)現(xiàn)的方法,Java 通過 JNI 機(jī)制調(diào)用。

雙親委派模型在JDK不同版本中有哪些變化?

JDK8中的三層類加載器:

JDK8以及之前的JDK版本都是如下三層類加載器實(shí)現(xiàn)方式。

1)啟動(dòng)類加載器(Bootstrap ClassLoader),這個(gè)類加載器是由C++實(shí)現(xiàn)的,負(fù)載加載$JAVA_HOME/jre/lib目錄下的jar文件,比如 rt.jar、tools.jar,或者-Xbootclasspath系統(tǒng)環(huán)境變量指定目錄下的路徑。它是個(gè)超級(jí)公民,即使開啟了Security Manager的時(shí)候,它也能擁有加載程序的所有權(quán)限,使用null作為擴(kuò)展類加載器的父類。

同時(shí),啟動(dòng)類加載器在JVM啟動(dòng)后也用于加載擴(kuò)展類加載器和系統(tǒng)類加載器。

獲取ClassLoader源碼

2)擴(kuò)展類加載器(Extension ClassLoader),這個(gè)類加載器由sun.misc.Launcher$ExtClassLoader來實(shí)現(xiàn),負(fù)責(zé)加載$JAVA_HOME/jre/lib/ext目錄中,或者java.ext.dirs系統(tǒng)變量指定路徑中所有的類庫,允許用戶將具備通用性的類庫可以放到ext目錄下,擴(kuò)展Java SE功能。在JDK 9之后,這種擴(kuò)展機(jī)制被模塊化帶來的天然的擴(kuò)展能力所取代。

3)應(yīng)用類加載器(App/System ClassLoader),也稱作為系統(tǒng)類加載器,這個(gè)類加載器由sun.misc.Launcher$AppClassLoader來實(shí)現(xiàn)。 它負(fù)責(zé)加載用戶應(yīng)用類路徑(ClassPath)上所有的類庫,開發(fā)者同樣可以直接在代碼中使用這個(gè)類加載器。如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下這個(gè)就是程序中默認(rèn)的類加載器。

JDK9中的類加載器有哪些變化?

1)擴(kuò)展類加載器被重命名為平臺(tái)類加載器(Platform ClassLoader),部分不需要 AllPermission 的 Java 基礎(chǔ)模塊,被降級(jí)到平臺(tái)類加載器中,相應(yīng)的權(quán)限也被更精細(xì)粒度地限制起來。

2) 擴(kuò)展類加載器機(jī)制被移除。這會(huì)帶來什么影響呢?就是說如果我們指定 java.ext.dirs 環(huán)境變量,或者 $JAVA_HOME/jre/lib/ext目錄存在,JVM會(huì)返回錯(cuò)誤。 建議解決辦法就是將其放入 classpath 里。部分不需要 AllPermission 的 Java 基礎(chǔ)模塊,被降級(jí)到平臺(tái)類加載器中,相應(yīng)的權(quán)限也被更精細(xì)粒度地限制起來。

3)在$JAVA_HOME/jre/lib路徑下的 rt.jar 和 tools.jar 同樣是被移除了。JDK 的核心類庫以及相關(guān)資源,被存儲(chǔ)在 jimage 文件中,并通過新的 JRT 文件系統(tǒng)訪問,而不是原有的 JAR 文件系統(tǒng)。

4)增加了 Layer 的抽象, JVM 啟動(dòng)默認(rèn)創(chuàng)建 BootLayer,開發(fā)者也可以自己去定義和實(shí)例化 Layer,可以更加方便的實(shí)現(xiàn)類似容器一般的邏輯抽象。

新增的Layer的抽象,去內(nèi)部的BootLayer作為內(nèi)建類加載器,包括了 BootStrap Loader、Platform Loader、Application Loader,其他 Layer 內(nèi)部有自定義的類加載器,不同版本模塊可以同時(shí)工作在不同的 Layer。

結(jié)合了 Layer,目前最新的 JVM 內(nèi)部結(jié)構(gòu)如下圖所示:

Layer JVM內(nèi)存結(jié)構(gòu)

5、Tomcat如何打破雙親委派模型的

因?yàn)镴DK里的類加載器ClassLoader是抽象類,如果你自定義類加載器可以重寫 findClass() 方法,重寫 findClass() 方法還是會(huì)按照既定的雙親委派機(jī)制運(yùn)作的。

而我們發(fā)現(xiàn)loadClass()方法也是public修飾的,說明也是允許重寫的,重寫loadClass()方法就可以『為所欲為』了,不按照既定套路出牌了,不遵循雙親委派模型。

典型的就是Tomcat應(yīng)用容器,就是自定義WebAppClassLoader類加載器,打破了雙親委派模型。

WebAppClassLoader 類加載器具體實(shí)現(xiàn)是重寫了 ClassLoader 的兩個(gè)方法:loadClass() 和 findClass()。其大致工作過程:首先類加載器自己嘗試去加載某個(gè)類,如果找不到再委托代理給父類加載器,其目的是優(yōu)先加載 Web 應(yīng)用自己定義的類。

這也正是一個(gè)Tomcat能夠部署多個(gè)應(yīng)用實(shí)例的根本原因。

接下來,我們分析下源碼實(shí)現(xiàn):

loadClass() 重寫方法的源碼實(shí)現(xiàn),僅保留最核心的代碼便于理解:

// 重寫了 loadClass() 方法
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

  // 使用了synchronized同步鎖
    synchronized (getClassLoadingLock(name)) {

            Class<?> clazz = null;

            //1)先在本地緩存中,查找該類是否已經(jīng)加載過
            clazz = findLoadedClass0(name);
            if (clazz != null) {
                    if (resolve)
                        // 本地緩存找到,連接該類
                            resolveClass(clazz);
                    return clazz;
            }

            //2) 從系統(tǒng)類加載器的緩存中,查找該類是否已經(jīng)加載過
            clazz = findLoadedClass(name);
            if (clazz != null) {
                    if (resolve)
                        // 從系統(tǒng)類加載器緩存找到,連接該類
                            resolveClass(clazz);
                    return clazz;
            }

            // 3)嘗試用ExtClassLoader類加載器類加載
            ClassLoader javaseLoader = getJavaseClassLoader();
            try {
                    clazz = javaseLoader.loadClass(name);
                    if (clazz != null) {
                            if (resolve)
                                //  從擴(kuò)展類加載器中找到,連接該類
                                    resolveClass(clazz);
                            return clazz;
                    }
            } catch (ClassNotFoundException e) {
                    // Ignore
            }

            // 4)嘗試在本地目錄查找加載該類
            try {
                    clazz = findClass(name);
                    if (clazz != null) {
                            if (resolve)
                                // 從本地目錄找到,連接該類
                                    resolveClass(clazz);
                            return clazz;
                    }
            } catch (ClassNotFoundException e) {
                    // Ignore
            }

            // 5) 嘗試用系統(tǒng)類加載器來加載
                    try {
                            clazz = Class.forName(name, false, parent);
                            if (clazz != null) {
                                    if (resolve)
                                        // 從系統(tǒng)類加載器中找到,連接該類
                                            resolveClass(clazz);
                                    return clazz;
                            }
                    } catch (ClassNotFoundException e) {
                            // Ignore
                    }
         }

    //6. 上述過程都加載失敗,拋出異常
    throw new ClassNotFoundException(name);
}

loadClass() 重寫的方法實(shí)現(xiàn)上會(huì)復(fù)雜些,畢竟打破雙親委派機(jī)制就在這里實(shí)現(xiàn)的。

主要有如下幾個(gè)步驟:

1)先在本地緩存 Cache 查找該類是否已經(jīng)加載過,即 Tomcat 自定義類加載器 WebAppClassLoader 是否已加載過。

2)如果 Tomcat 類加載器沒有加載過這個(gè)類,再看看系統(tǒng)類加載器是否加載過。

3)如果系統(tǒng)類加載器也沒有加載過,此時(shí),會(huì)讓 ExtClassLoader 擴(kuò)展類加載器去加載,很關(guān)鍵,其目的防止 Web 應(yīng)用自己的類覆蓋 JRE 的核心類。

因?yàn)?Tomcat 需要打破雙親委托機(jī)制,假如 Web 應(yīng)用里有類似上面舉的例子自定義了 Object 類,如果先加載這些JDK中已有的類,會(huì)導(dǎo)致覆蓋掉JDK里面的那個(gè) Object 類。

這就是為什么 Tomcat 的類加載器會(huì)優(yōu)先嘗試用 ExtClassLoader 去加載,因?yàn)?ExtClassLoader 會(huì)委托給 BootstrapClassLoader 去加載,JRE里的類由BootstrapClassLoader安全加載,然后返回給 Tomcat 的類加載器。

這樣 Tomcat 的類加載器就不會(huì)去加載 Web 應(yīng)用下的 Object 類了,也就避免了覆蓋 JRE 核心類的問題。

4)如果 ExtClassLoader 加載器加載失敗,也就是說 JRE 核心類中沒有這類,那么就在本地 Web 應(yīng)用目錄下查找并加載。

5)如果本地目錄下沒有這個(gè)類,說明不是 Web 應(yīng)用自己定義的類,那么由系統(tǒng)類加載器去加載。這里請(qǐng)你注意:Web 應(yīng)用是通過Class.forName調(diào)用交給系統(tǒng)類加載器的,因?yàn)镃lass.forName的默認(rèn)加載器就是系統(tǒng)類加載器。

6)如果上述加載過程全部失敗,拋出 ClassNotFoundException 異常。

findClass() 重寫方法的源碼實(shí)現(xiàn),僅展示最核心代碼便于理解:

// 重寫了 findClass 方法
public Class<?> findClass(String name) throws ClassNotFoundException {
    ...
    
    Class<?> clazz = null;
    try {
                    //1) 優(yōu)先在自己Web應(yīng)用目錄下查找類 
                    clazz = findClassInternal(name);
    }  catch (RuntimeException e) {
                 throw e;
         }

    if (clazz == null) {
    try {
                    //2) 如果在本地目錄沒有找到當(dāng)前類,則委托代理給父加載器去查找
                    clazz = super.findClass(name);
    }  catch (RuntimeException e) {
                 throw e;
         }

    //3) 如果父類加載器也沒找到,則拋出ClassNotFoundException
    if (clazz == null) {
            throw new ClassNotFoundException(name);
     }

    return clazz;
}

在 findClass() 重寫的方法里,主要有三個(gè)步驟:

1)先在 Web 應(yīng)用本地目錄下查找要加載的類。

2)如果沒有找到,交給父加載器去查找,它的父加載器就是上面提到的系統(tǒng)類加載器 AppClassLoader。

3)如何父加載器也沒找到這個(gè)類,拋出 ClassNotFoundException 異常。

6、上下文類加載器深入淺出剖析

我們都知道Jdbc是一個(gè)標(biāo)準(zhǔn),那么具體數(shù)據(jù)庫廠商會(huì)根據(jù)Jdbc標(biāo)準(zhǔn)提供自己的數(shù)據(jù)庫實(shí)現(xiàn),既然Jdbc是一個(gè)標(biāo)準(zhǔn),這些類原生的會(huì)存在JDK中了,比如Connection、Statement,而且是位于rt.jar包中的,他們?cè)趩?dòng)的時(shí)候是由BootstrapClassLoader加載的。

那么怎么具體加載廠商的實(shí)現(xiàn)呢?

肯定是通過廠商提供相應(yīng)的jar包,然后放到我們應(yīng)用的ClassPath下,這樣的話,廠商所提供的jar中的肯定不是由啟動(dòng)類加載器去加載的。

所以,廠商的具體驅(qū)動(dòng)的實(shí)現(xiàn)是由應(yīng)用類加載器進(jìn)行加載的 。

Connection是一個(gè)接口,它是由啟動(dòng)類加載器加載的,而它具體的實(shí)現(xiàn)啟動(dòng)類加載器無法加載,由系統(tǒng)類加載器加載的。這樣會(huì)存在什么樣的問題?

根據(jù)**類加載原則: **

  • 父類加載器的加載類或接口是看不到子類加載器加載的類或接口的。
  • 子加載器所加載的類或接口是能看到父加載器加載的類或接口的。

SPI(Service Provider Interface)

父ClassLoader可以使用當(dāng)前線程Thread.currentThread().getContextClassLoader()所指定的classloader加載的類。

這就改變了父ClassLoader不能使用子ClassLoader或是其他沒有直接父子關(guān)系的ClassLoader所加載類的情況,即改變了雙親委托模型。

線程上下文類加載器就是當(dāng)前線程的Current Classloader。

在雙親委托模型下,類加載器是由下而上,即下層的類加載器會(huì)委托上層進(jìn)行加載。但是對(duì)于SPI來說,有些接口是Java核心庫所提供的,而Java核心庫是由啟動(dòng)類加載器來加載的,而這些接口的實(shí)現(xiàn)卻來自于不同jar包(廠商提供),Java的啟動(dòng)類加載器是不會(huì)加載其他來源的jar包,這樣傳統(tǒng)的雙親委托模型就無法滿足SPI的要求。

而通過給當(dāng)前線程設(shè)置上下文類加載器,就可以由設(shè)置的上下文類加載器來實(shí)現(xiàn)對(duì)于接口實(shí)現(xiàn)類的加載。

線程上下文類加載器的一般使用模式:

獲取 ---> 使用 --> 還原

ClassLoader classloader = Thread.currentThread().getContextClassLoader();
try {
     // 將目標(biāo)類加載器設(shè)置到上下文類加載器
     Thread.currentThread().setContextClassLoader(targetTccl); 
         // 在該方法中使用設(shè)置的上下文類加載器加載所需的類
     doSomethingUsingContextClassLoader(); 
} finally {
    // 將原來的classloader設(shè)置到上下文類加載器
    Thread.currentThread().setContextClassLoader(classloader); 
}

doSomethingUsingContextClassLoader()方法中則調(diào)用了 Thread.currentThread().getContextClassLoader() ,獲取當(dāng)前線程的上下文類加載器做某些事情。
如果一個(gè)類由類加載器A加載,那么這個(gè)類的依賴類也是由相同的類加載器加載的(如果該依賴類之前沒有被加載過的話)。

在SPI的接口代碼當(dāng)中,就可以通過上下文類加載器成功的加載到SPI的實(shí)現(xiàn)類。因此,上下文類加載器在很多的SPI的實(shí)現(xiàn)中都會(huì)得到大量的應(yīng)用。

當(dāng)高層提供了統(tǒng)一的接口讓低層(比如Jdbc各個(gè)廠商提供的具體實(shí)現(xiàn)類)去實(shí)現(xiàn),同時(shí)又要在高層加載(或?qū)嵗┑蛯拥念悤r(shí),就必須要通過線程上下文類加載器來幫助高層的類加載器并加載該類(本質(zhì)上,高層的類加載器與低層的類加載器是不一樣的)

一般情況下,我們沒有修改過線程上下文類加載器,默認(rèn)的就是系統(tǒng)類加載器。由于是運(yùn)行期間是設(shè)置的上下文類加載器,所以,不管當(dāng)前程序在什么地方,在啟動(dòng)類的加載器的范圍內(nèi)還是擴(kuò)展類加載器的范圍內(nèi),那么我們?cè)谌魏斡行枰臅r(shí)候都是可以通過Thread.currentThread().getContextClassLoader()獲取設(shè)置的上下文類加載器來完成操作。

這個(gè)也有點(diǎn)像ThreadLocal的類,如果借助于ThreadLocal的話就沒有必要同步,因?yàn)槊恳粋€(gè)線程都有相應(yīng)的數(shù)據(jù)副本,這些數(shù)據(jù)副本之間是互不干擾的,他們只能被當(dāng)前的線程所使用和訪問,既然每個(gè)線程都有數(shù)據(jù)副本,每個(gè)線程當(dāng)然操作的是副本,所以線程之間就不需要同步、鎖就可以處理并發(fā)。ThreadLocal本質(zhì)上是用空間換時(shí)間的概念,因?yàn)槲覀儗?shù)據(jù)拷貝多份會(huì)占用一定的內(nèi)存空間,每個(gè)線程中去使用。

7、最后的總結(jié)

限于篇幅,本文主要對(duì)類的初始化時(shí)機(jī),類的加載過程中最重要的類加載器機(jī)制進(jìn)行了分析,對(duì)其中的雙親委派模型,以及Tomcat是如何打破雙親委派模型的,結(jié)合源代碼進(jìn)行了深入剖析,對(duì)上下文類加載器是如何改變雙親委派模型進(jìn)行了分析。

總結(jié)一下:

一個(gè)類都是通過主動(dòng)使用的方式加載到JVM當(dāng)中的,到目前為止一共總結(jié)了八種情況,除此之外的都屬于被動(dòng)使用,被動(dòng)使用的列舉了代碼示例,結(jié)合示例可以更為清晰的理解。

詳細(xì)介紹了雙親委派模型的工作過程,JDK8和JDK9版本中類加載器層次關(guān)系,類加載器的結(jié)果本質(zhì)上并不是一種樹形結(jié)構(gòu),而是一種包含關(guān)系。

同時(shí),也介紹了Tomcat是如何打破雙親委派機(jī)制的,通過源碼透視打破規(guī)則的全過程。

最后,對(duì)上下文類加載器根據(jù)Jdbc的例子,進(jìn)一步分析了使用模式,如何改變雙親委派機(jī)制做到父類加載器,可以加載和使用各個(gè)廠商提供的實(shí)現(xiàn)類的。

另外,回到最初的圖示,一個(gè)類要想順利進(jìn)入到JVM內(nèi)存結(jié)構(gòu)中,除了類的加載階段外,還有驗(yàn)證、準(zhǔn)備、解析、初始化四個(gè)階段完成后,才算真正完成類的初始化操作。
在JVM中某個(gè)類的Class對(duì)象不再被引用,即不可觸及,Class對(duì)象就會(huì)結(jié)束生命周期,該類在方法區(qū)內(nèi)的數(shù)據(jù)會(huì)被卸載,從而技術(shù)該類的整個(gè)生命周期。

一個(gè)類何時(shí)結(jié)束生命周期,取決于代表它的Class對(duì)象何時(shí)結(jié)束生命周期。

但是,JVM自帶的類加載器所加載的類,在虛擬機(jī)的生命周期中,始終不會(huì)被卸載。前面已經(jīng)介紹過,JVM自帶的類加載器包括引導(dǎo)類加載器、擴(kuò)展類加載器和系統(tǒng)類加載器(應(yīng)用類加載器)。Java虛擬機(jī)本身會(huì)始終引用這些類加載器,而這些類加載器會(huì)始終引用它們所加載的類的Class對(duì)象,因此這些Class對(duì)象是始終可觸及的。

在如下情況下,JVM將結(jié)束生命周期。

  • 執(zhí)行了System.exit()

  • 程序正常執(zhí)行結(jié)束

  • 程序在執(zhí)行過程中遇到了異?;蛘咤e(cuò)誤而異常終止

  • 由于操作系統(tǒng)出現(xiàn)錯(cuò)誤而導(dǎo)致Java虛擬機(jī)進(jìn)程終止

大家如何覺得本文有收獲關(guān)個(gè)注唄,碼字不易,文章不妥之處,歡迎留言斧正。本號(hào)不定期會(huì)發(fā)布精彩原創(chuàng)文章。

參考資料:

深入理解Java虛擬機(jī)

極客時(shí)間課程

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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