JVM類加載和雙親委派機(jī)制

類加載器

類加載器的作用就是把磁盤中的類文件加載到內(nèi)存的方法區(qū)以供使用,分析類加載前,先看下jvm運(yùn)行時(shí)都需要加載什么樣的類

類和類庫

jvm運(yùn)行時(shí)的類主要分三種

  • 核心類
    比如String,Thread,Lock等,這種類都是jdk提供的,我們不需要自己寫,路徑在JRE的lib目錄下
  • 擴(kuò)展類
    一些jdk提供的擴(kuò)展類,比如DESKey等加密的類,也不需要自己寫,路徑是JRE的lib目錄下的ext擴(kuò)展目錄
  • 應(yīng)用程序類
    也就是程序員自己寫的類

三種不同的類存儲(chǔ)在磁盤不同的目錄下,jvm運(yùn)行時(shí)需要類加載器把類信息加載到內(nèi)存,具體說是工作區(qū)中,而針對(duì)三種類型的類分別是三種不同的類加載器負(fù)責(zé)加載的

引導(dǎo)類加載器/核心類加載器

jvm啟動(dòng)時(shí)首先會(huì)創(chuàng)建一個(gè)引導(dǎo)類加載器實(shí)例,是由C++實(shí)現(xiàn)的,它的作用就是加載支撐JVM運(yùn)行時(shí)的核心類庫,這些類庫所在地就是我們jdk安裝路徑下的jre/lib中的一些jar包

jre/lib

其中最核心的就是rt.jar,我們使用的大部分基礎(chǔ)類比如:String,Thread都定義在這個(gè)包下

擴(kuò)展類加載器

借助C++,有了引導(dǎo)類加載器,就可以實(shí)現(xiàn)加載一些基本的類,同時(shí)也可以定義一個(gè)自己的(java)的類加載器的基本類,由引導(dǎo)類加載器加載到工作區(qū),就可以創(chuàng)建它的實(shí)例,用這個(gè)實(shí)例就可自己去加載其他的類:額外類和應(yīng)用程序類,就不用再麻煩C++實(shí)現(xiàn)的引導(dǎo)類加載器,畢竟java就可以做了
這就好比雞生蛋問題,一個(gè)農(nóng)夫要吃雞蛋,先從外部買個(gè)雞,下了雞蛋又可生成雞繼續(xù)下蛋,就不需要再去買雞了

rt.jar類庫中還有一個(gè)重量級(jí)的類:sun.misc.Launcher,翻譯過來就是啟動(dòng)器,jvm在啟動(dòng)時(shí)通過C++實(shí)現(xiàn)的引導(dǎo)類加載器加載了這個(gè)類,然后生成一個(gè)該類的實(shí)例,實(shí)例初始化的構(gòu)造方法如下

Launcher

其中Launcher.ExtClassLoader.getExtClassLoader()
就是初始化一個(gè)擴(kuò)展類加載器實(shí)例,它可以把JRE的lib目錄下的ext擴(kuò)展目錄下的類庫加載到工作區(qū)

應(yīng)用程序類加載器

而下面的Launcher.AppClassLoader.getAppClassLoader(var1)就是初始化一個(gè)應(yīng)用程序類加載器實(shí)例,它可以把用戶代碼加載到工作區(qū)

解釋

這里可能有點(diǎn)蒙,可以用下面例子幫助理解一下

我們把jvm比作一個(gè)造萬物的工廠,工廠可以生產(chǎn)任何實(shí)物(對(duì)象實(shí)例),前提是必須有圖紙(class)才能創(chuàng)建出來

工廠工作時(shí)為了查找方便把生產(chǎn)出的實(shí)物和圖紙分開存儲(chǔ),其中圖紙存入工作方法區(qū),實(shí)物存入工作堆區(qū)

工廠向外部提供服務(wù),客戶給一張圖紙,工廠就可以生產(chǎn)出實(shí)物

同時(shí)工廠提供一些基本組件的圖紙(核心類),客戶可以在自己的圖紙中標(biāo)志使用這些基本組件

工廠提供一些額外組件的圖紙(擴(kuò)展類),滿足特殊需求,客戶也可以在自己的圖紙中標(biāo)志使用這些額外組件

但是客戶給的圖紙都放在一個(gè)圖紙收集欄中(java.class.path),工廠需要收集欄圖紙加載到工作方法區(qū),并做一些特殊處理。與此同時(shí)工廠提供的基本組件圖紙和額外圖紙(放在倉庫里)也需要首先加載到工作方法區(qū),這樣才能在實(shí)際生產(chǎn)客戶實(shí)物用到時(shí)使用

為了實(shí)現(xiàn)加載這個(gè)加載過程,工廠首先從其它工廠借了個(gè)引導(dǎo)加載器(C++實(shí)現(xiàn)),它的工作就是把基本組件圖紙從倉庫加載到工作方法區(qū)

有了這個(gè)引導(dǎo)加載器,只能加載基本組件圖紙,客戶提交的圖紙和額外圖紙還是無法加載

于是工廠自己生成加載器,在基本組件圖紙倉庫中中制作了“額外圖紙加載器”和“客戶圖紙加載器”的圖紙

工廠開始工作后,有了引導(dǎo)類加載器的加載,就拿到了兩張自定義加載器的圖紙,然后創(chuàng)建這個(gè)“額外圖紙加載器”和“客戶圖紙加載器”的實(shí)物,

然后用“額外圖紙加載器”可以把額外的圖紙加載進(jìn)工作方法區(qū)

再用“客戶圖紙加載器”可以把客戶提交的圖紙加載到工作方法區(qū), 就這樣所有生產(chǎn)所需的圖紙都可以獲取到了,便可實(shí)現(xiàn)生產(chǎn)

查看類加載器

有了這三種類加載器,我們程序所需的類都可以加載到工作區(qū),進(jìn)而生成類的實(shí)例,我們可以使用代碼一探這些加載器

public class TestJDKClassLoader {
    public static void main(String[] args) {
            System.out.println(String.class.getClassLoader()); // String的類加載器
            System.out.println(DESKeyFactory.class.getClassLoader().getClass().getName());
            System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName());
            System.out.println(ClassLoader.getSystemClassLoader());
    }
}

輸出

null
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$AppClassLoader

可以看到String.class的類加載器是null,因?yàn)橐龑?dǎo)類加載器是C++的對(duì)象,所以java打印不出來
DESKeyFactory.class在ext包下,所以它的類加載器是ExtClassLoader(擴(kuò)展類加載器)
TestJDKClassLoader是自己實(shí)現(xiàn)的類,對(duì)應(yīng)加載器AppClassLoader(應(yīng)用類加載器)
另外一種獲取類加載器的方式ClassLoader.getSystemClassLoader()

類加載時(shí)機(jī)

上文一直說類加載“可以”加載,那么實(shí)際上這些類什么時(shí)候被加載吶,可以做個(gè)測(cè)試

這里補(bǔ)充一下static代碼塊的代碼是類加載后執(zhí)行的

public class TestDynamicLoad {

    static {
        System.out.println("*************load TestDynamicLoad************");
    }

    public static void main(String[] args) {
        new A();
        B b = null;  //B不會(huì)加載,除非這里執(zhí)行 new B()
    }
}

class A {
    static {
        System.out.println("*************load A************");
    }

    public A() {
        System.out.println("*************initial A************");
    }
}

class B {
    static {
        System.out.println("*************load B************");
    }

    public B() {
        System.out.println("*************initial B************");
    }
}

運(yùn)行結(jié)果:
*************load TestDynamicLoad************
*************load A************
*************initial A************

可以看到Class B并沒有被實(shí)際加載,說明類加載也是一種懶加載,用到才使用類加載器加載

雙親委派機(jī)制

層級(jí)結(jié)構(gòu)

上文介紹了三種不同的類加載器分別加載不同類型的類,其實(shí)他們仨除了分工不同,還有上下級(jí)的關(guān)系,來看一下ClassLoader(類加載器的抽象)源碼

ClassLoader

一個(gè)類加載器中會(huì)包含一個(gè)parent屬性指向父類加載器,形成了一個(gè)單項(xiàng)鏈表,而以上三種類加載器的父子結(jié)構(gòu)是這樣的

結(jié)構(gòu)

類加載機(jī)制

有了這個(gè)層級(jí)有什么用吶,這就涉及到類加載機(jī)制,也就是雙親委派機(jī)制,機(jī)制其實(shí)很簡單:因?yàn)槊總€(gè)類加載器負(fù)責(zé)的類地址不一樣,所以當(dāng)一個(gè)類加載器想加載一個(gè)類時(shí)先讓父節(jié)點(diǎn)去父地盤找,找不到子節(jié)點(diǎn)再找,遞歸下去最終結(jié)果就是,要加載一個(gè)類,依次去引導(dǎo)類加載器>擴(kuò)展類加載器>應(yīng)用類加載器尋找,找到后就加載,看一下ClassLoader.loadClass代碼

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
        synchronized (getClassLoadingLock(name)) {
            // 0.檢查是否已加載,如果是就不用重新加載
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) { // 1.1 如果父節(jié)點(diǎn)不是null,讓父節(jié)點(diǎn)先查
                        c = parent.loadClass(name, false);
                    } else { //1.2 如果父節(jié)點(diǎn)是null,即引導(dǎo)類加載器,去查找基礎(chǔ)類
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }

                if (c == null) { //2. 父類查不到,自己找
                    // If still not found, then invoke findClass in order to find the class.
                    long t1 = System.nanoTime();
                    // 自己找的方法
                    c = findClass(name);
                    ...                

示意圖如下

雙親委派機(jī)制

雙親委派機(jī)制的作用

以上介紹了雙親委派機(jī)制,那為什么要這么設(shè)計(jì),主要是防止核心api被篡改,比如你自己寫一個(gè)String類且包名和原類一致,但由于雙親委派機(jī)制的存在,你寫的永遠(yuǎn)不會(huì)被加載。

比如說上例的工廠已經(jīng)提供了標(biāo)準(zhǔn)螺絲釘?shù)脑O(shè)計(jì)圖,如果客戶意圖使用自己的螺絲釘設(shè)計(jì)圖替換工廠的是不行的

自定義類加載器

以上三種類加載器,除了引導(dǎo)類加載器,都是java實(shí)現(xiàn)的,那作為java程序員可不可以自己寫一個(gè)類加載器?答案是肯定的,比如我們可以繼承上文提到的ClassLoader抽象類,嘗試寫一個(gè)類加載器來把桌面上存放的的類加載進(jìn)來

ClassLoader實(shí)際查找累的方法是findClass,也就是上文loadClass方法中當(dāng)父節(jié)點(diǎn)差找不到類時(shí)所執(zhí)行的方法,默認(rèn)如下

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

默認(rèn)拋出異常,也就是說要想實(shí)現(xiàn)類加載器,這個(gè)方法肯定要重寫的,最終代碼如下

public class MyClassLoader extends ClassLoader {
    /**
     * 桌面路徑
     */
    private String classPath = "C:/Users/Administrator/Desktop";

    private byte[] loadByte(String name) throws Exception {
        name = name.replaceAll("\\.", "/");
        FileInputStream fis = new FileInputStream(classPath + "/" + name
                + ".class");
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();
        return data;
    }

     /**
     * 重寫findClass方法
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] data = loadByte(name);
            //defineClass將一個(gè)字節(jié)數(shù)組轉(zhuǎn)為Class對(duì)象,這個(gè)字節(jié)數(shù)組是class文件讀取后最終的字節(jié)數(shù)組。
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }
}

測(cè)試一下,創(chuàng)建一個(gè)類放入桌面,包名是com,所以放一個(gè)com文件夾并放進(jìn)去,并通過javac轉(zhuǎn)換為class文件

package com;
public class MyClass {
    public void say() {
        System.out.println("hello");
    }
}
桌面建com包
存class文件

測(cè)試代碼

public static void main(String[] args) throws Exception {
    MyClassLoader classLoader = new MyClassLoader();
    // 獲取MyClass類
    Class clazz = classLoader.loadClass("com.MyClass");
    Object obj = clazz.newInstance();
    Method method = clazz.getDeclaredMethod("say", null);
    method.invoke(obj, null);
    System.out.println(clazz.getClassLoader().getClass().getName()); // 輸出hello
}

這樣我們就實(shí)現(xiàn)了一個(gè)自定義類加載器,可以把各個(gè)地方的類文件加載進(jìn)來并運(yùn)行

如果打印自定義加載器的父加載器就是AppClassLoader,是因?yàn)?code>ClassLoader抽象類中有個(gè)無參構(gòu)造函數(shù)(子類實(shí)例化會(huì)調(diào)用父類無參構(gòu)造)

protected ClassLoader() {
    this(checkCreateClassLoader(), getSystemClassLoader()); // getSystemClassLoader()的結(jié)果就是AppClassLoader
}

因此用自定義加載器去加載String.class也行的通,因?yàn)殡p親委派會(huì)去父級(jí)先找

打破雙親委派機(jī)制

“打破雙親委派機(jī)制”好像總被提起,聽起來很高端,其實(shí)看代碼雙親委派機(jī)制不過是對(duì)ClassLoader.loadClass方法執(zhí)行過程起的一個(gè)名字,也就是說只是ClassLoader.loadClass實(shí)現(xiàn)了雙親委派機(jī)制,那作為子類完全可以覆蓋重寫,所以所謂打破,也不過就是子類覆蓋重寫了父類的默認(rèn)代碼而已

比如說ClassLoader.loadClass的邏輯是先去父級(jí)找,找不到再findClass,我們可以改成先findClass,找不到再調(diào)用父級(jí)的loadClass不就打破了嗎

Tomcat

提到“打破雙親委派機(jī)制”,就不得不提Tomcat,因?yàn)樗褪且粋€(gè)打破雙親委派機(jī)制的典型案例

Tomcat為什么要打破這個(gè)機(jī)制,主要因?yàn)橐粋€(gè)tomcat容器可能同時(shí)運(yùn)行多個(gè)項(xiàng)目,多項(xiàng)目可能都有一樣報(bào)名和類名的類,但功能不一樣,比如引入不同的版本的三方類庫這種問題就太多了:

比如說項(xiàng)目一引用了1.0版本的某框架,項(xiàng)目二引用了同一框架的2.0版本,那么某類被加載一次就不會(huì)再被加載兩個(gè)項(xiàng)目其實(shí)用到的都是同一版本的某類,這樣肯定就錯(cuò)錯(cuò)了

所以Tomcat必須打破原機(jī)制,也就是重寫loadClass實(shí)現(xiàn)自己的一套隔離版的類加載機(jī)制

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容