2.java類加載器classloader,從字節(jié)碼到j(luò)vm

1.classloader是干什么的?

根據(jù)字面意思,也可以清楚,classloader是用來加載class的。其功能即為,將class的字節(jié)碼形式轉(zhuǎn)換成內(nèi)存形式的class對象。字節(jié)碼可以來自jar包中的.class,可以是磁盤中的.class,甚至是來自遠(yuǎn)程服務(wù)器的字節(jié)流數(shù)組。因?yàn)樽止?jié)碼的本質(zhì)就是一個byte[],它有特定的內(nèi)部格式而已。

image.png

class對象的內(nèi)部有一個classloader字段來標(biāo)識自己是被哪個classloader加載的,classloader就是一個容器,里面裝了很多已經(jīng)被加載的calss對象。

 /*
     * Private constructor. Only the Java Virtual Machine creates Class objects.
     * This constructor is not used and prevents the default constructor being
     * generated.
     */
    private Class(ClassLoader loader) {
        // Initialize final field for classLoader.  The initialization value of non-null
        // prevents future JIT optimizations from assuming this final field is null.
        classLoader = loader;
    }

2.特點(diǎn):延遲加載

JVM 運(yùn)行并不是一次性加載所需要的全部類的,它是按需加載,也就是延遲加載。程序在運(yùn)行的過程中會逐漸遇到很多不認(rèn)識的新類,這時候就會調(diào)用 ClassLoader 來加載這些類。加載完成后就會將 Class 對象存在 ClassLoader 里面,下次就不需要重新加載了。

比如你在調(diào)用某個類的靜態(tài)方法時,首先這個類肯定是需要被加載的,但是并不會觸及這個類的實(shí)例字段,那么實(shí)例字段的類別 Class 就可以暫時不必去加載,但是它可能會加載靜態(tài)字段相關(guān)的類別,因?yàn)殪o態(tài)方法會訪問靜態(tài)字段。而實(shí)例字段的類別需要等到你實(shí)例化對象的時候才可能會加載。

3.三種類加載器

JVM 運(yùn)行實(shí)例中會存在多個 ClassLoader,不同的 ClassLoader 會從不同的地方加載字節(jié)碼文件。它可以從不同的文件目錄加載,也可以從不同的 jar 文件中加載,也可以從網(wǎng)絡(luò)上不同的服務(wù)地址來加載。

JVM 中內(nèi)置了三個重要的 ClassLoader,分別是 BootstrapClassLoader、ExtensionClassLoader 和 AppClassLoader。

BootstrapClassLoader 負(fù)責(zé)加載 JVM 運(yùn)行時核心類,這些類位于 JAVA_HOME/lib/rt.jar 文件中,我們常用內(nèi)置庫 java.xxx.* 都在里面,比如 java.util.、java.io.、java.nio.、java.lang. 等等。這個 ClassLoader 比較特殊,它是由 C 代碼實(shí)現(xiàn)的,我們將它稱之為「根加載器」。

ExtensionClassLoader 負(fù)責(zé)加載 JVM 擴(kuò)展類,比如 swing 系列、內(nèi)置的 js 引擎、xml 解析器 等等,這些庫名通常以 javax 開頭,它們的 jar 包位于 JAVA_HOME/lib/ext/*.jar 中,有很多 jar 包。

AppClassLoader 才是直接面向我們用戶的加載器,它會加載 Classpath 環(huán)境變量里定義的路徑中的 jar 包和目錄。我們自己編寫的代碼以及使用的第三方 jar 包通常都是由它來加載的。

那些位于網(wǎng)絡(luò)上靜態(tài)文件服務(wù)器提供的 jar 包和 class文件,jdk 內(nèi)置了一個 URLClassLoader,用戶只需要傳遞規(guī)范的網(wǎng)絡(luò)路徑給構(gòu)造器,就可以使用 URLClassLoader 來加載遠(yuǎn)程類庫了。URLClassLoader 不但可以加載遠(yuǎn)程類庫,還可以加載本地路徑的類庫,取決于構(gòu)造器中不同的地址形式。ExtensionClassLoader 和 AppClassLoader 都是 URLClassLoader 的子類,它們都是從本地文件系統(tǒng)里加載類庫。

AppClassLoader 可以由 ClassLoader 類提供的靜態(tài)方法 getSystemClassLoader() 得到,它就是我們所說的「系統(tǒng)類加載器」,我們用戶平時編寫的類代碼通常都是由它加載的。當(dāng)我們的 main 方法執(zhí)行的時候,這第一個用戶類的加載器就是 AppClassLoader。

4.ClassLoader 傳遞性

程序在運(yùn)行過程中,遇到了一個未知的類,它會選擇哪個 ClassLoader 來加載它呢?虛擬機(jī)的策略是使用調(diào)用者 Class 對象的 ClassLoader 來加載當(dāng)前未知的類。何為調(diào)用者 Class 對象?就是在遇到這個未知的類時,虛擬機(jī)肯定正在運(yùn)行一個方法調(diào)用(靜態(tài)方法或者實(shí)例方法),這個方法掛在哪個類上面,那這個類就是調(diào)用者 Class 對象。前面我們提到每個 Class 對象里面都有一個 classLoader 屬性記錄了當(dāng)前的類是由誰來加載的。

因?yàn)?ClassLoader 的傳遞性,所有延遲加載的類都會由初始調(diào)用 main 方法的這個 ClassLoader 全全負(fù)責(zé),它就是 AppClassLoader。

5.雙親委派原則
前面我們提到 AppClassLoader 只負(fù)責(zé)加載 Classpath 下面的類庫,如果遇到?jīng)]有加載的系統(tǒng)類庫怎么辦,AppClassLoader 必須將系統(tǒng)類庫的加載工作交給 BootstrapClassLoader 和 ExtensionClassLoader 來做,這就是我們常說的「雙親委派」。


image.png

AppClassLoader 在加載一個未知的類名時,它并不是立即去搜尋 Classpath,它會首先將這個類名稱交給 ExtensionClassLoader 來加載,如果 ExtensionClassLoader 可以加載,那么 AppClassLoader 就不用麻煩了。否則它就會搜索 Classpath 。

而 ExtensionClassLoader 在加載一個未知的類名時,它也并不是立即搜尋 ext 路徑,它會首先將類名稱交給 BootstrapClassLoader 來加載,如果 BootstrapClassLoader 可以加載,那么 ExtensionClassLoader 也就不用麻煩了。否則它就會搜索 ext 路徑下的 jar 包。

這三個 ClassLoader 之間形成了級聯(lián)的父子關(guān)系,每個 ClassLoader 都很懶,盡量把工作交給父親做,父親干不了了自己才會干。每個 ClassLoader 對象內(nèi)部都會有一個 parent 屬性指向它的父加載器。

class ClassLoader {
  ...
  private final ClassLoader parent;
  ...
}

值得注意的是圖中的 ExtensionClassLoader 的 parent 指針畫了虛線,這是因?yàn)樗?parent 的值是 null,當(dāng) parent 字段是 null 時就表示它的父加載器是「根加載器」。如果某個 Class 對象的 classLoader 屬性值是 null,那么就表示這個類也是「根加載器」加載的。

6.Class.forName方法

當(dāng)我們在使用 jdbc 驅(qū)動時,經(jīng)常會使用 Class.forName 方法來動態(tài)加載驅(qū)動類。

Class.forName("com.mysql.cj.jdbc.Driver");

其原理是 mysql 驅(qū)動的 Driver 類里有一個靜態(tài)代碼塊,它會在 Driver 類被加載的時候執(zhí)行。這個靜態(tài)代碼塊會將 mysql 驅(qū)動實(shí)例注冊到全局的 jdbc 驅(qū)動管理器里。

class Driver {
  static {
    try {
       java.sql.DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
       throw new RuntimeException("Can't register driver!");
    }
  }
  ...
}

forName 方法同樣也是使用調(diào)用者 Class 對象的 ClassLoader 來加載目標(biāo)類。不過 forName 還提供了多參數(shù)版本,可以指定使用哪個 ClassLoader 來加載

Class<?> forName(String name, boolean initialize, ClassLoader cl)

通過這種形式的 forName 方法可以突破內(nèi)置加載器的限制,通過使用自定類加載器允許我們自由加載其它任意來源的類庫。根據(jù) ClassLoader 的傳遞性,目標(biāo)類庫傳遞引用到的其它類庫也將會使用自定義加載器加載。

7.可以自定義加載器嗎

ClassLoader 里面有三個重要的方法 loadClass()、findClass() 和 defineClass()。

loadClass() 方法是加載目標(biāo)類的入口,它首先會查找當(dāng)前 ClassLoader 以及它的雙親里面是否已經(jīng)加載了目標(biāo)類,如果沒有找到就會讓雙親嘗試加載,如果雙親都加載不了,就會調(diào)用 findClass() 讓自定義加載器自己來加載目標(biāo)類。ClassLoader 的 findClass() 方法是需要子類來覆蓋的,不同的加載器將使用不同的邏輯來獲取目標(biāo)類的字節(jié)碼。拿到這個字節(jié)碼之后再調(diào)用 defineClass() 方法將字節(jié)碼轉(zhuǎn)換成 Class 對象。下面我使用偽代碼表示一下基本過程

class ClassLoader {

  // 加載入口,定義了雙親委派規(guī)則
  Class loadClass(String name) {
    // 是否已經(jīng)加載了
    Class t = this.findFromLoaded(name);
    if(t == null) {
      // 交給雙親
      t = this.parent.loadClass(name)
    }
    if(t == null) {
      // 雙親都不行,只能靠自己了
      t = this.findClass(name);
    }
    return t;
  }

  // 交給子類自己去實(shí)現(xiàn)
  Class findClass(String name) {
    throw ClassNotFoundException();
  }

  // 組裝Class對象
  Class defineClass(byte[] code, String name) {
    return buildClassFromCode(code, name);
  }
}

class CustomClassLoader extends ClassLoader {

  Class findClass(String name) {
    // 尋找字節(jié)碼
    byte[] code = findCodeFromSomewhere(name);
    // 組裝Class對象
    return this.defineClass(code, name);
  }
}

自定義類加載器不易破壞雙親委派規(guī)則,不要輕易覆蓋 loadClass 方法。否則可能會導(dǎo)致自定義加載器無法加載內(nèi)置的核心類庫。在使用自定義加載器時,要明確好它的父加載器是誰,將父加載器通過子類的構(gòu)造器傳入。如果父類加載器是 null,那就表示父加載器是「根加載器」。

// ClassLoader 構(gòu)造器
protected ClassLoader(String name, ClassLoader parent);

雙親委派規(guī)則可能會變成三親委派,四親委派,取決于你使用的父加載器是誰,它會一直遞歸委派到根加載器。

8.classLoader意義:分工與合作
這里我們重新理解一下 ClassLoader 的意義,它相當(dāng)于類的命名空間,起到了類隔離的作用。位于同一個 ClassLoader 里面的類名是唯一的,不同的 ClassLoader 可以持有同名的類。ClassLoader 是類名稱的容器,是類的沙箱。


image.png

不同的 ClassLoader 之間也會有合作,它們之間的合作是通過 parent 屬性和雙親委派機(jī)制來完成的。parent 具有更高的加載優(yōu)先級。除此之外,parent 還表達(dá)了一種共享關(guān)系,當(dāng)多個子 ClassLoader 共享同一個 parent 時,那么這個 parent 里面包含的類可以認(rèn)為是所有子 ClassLoader 共享的。這也是為什么 BootstrapClassLoader 被所有的類加載器視為祖先加載器,JVM 核心類庫自然應(yīng)該被共享。

9,實(shí)例:Thread.contextClassLoader
如果你稍微閱讀過 Thread 的源代碼,你會在它的實(shí)例字段中發(fā)現(xiàn)有一個字段非常特別

class Thread {
  ...
  private ClassLoader contextClassLoader;

  public ClassLoader getContextClassLoader() {
    return contextClassLoader;
  }

  public void setContextClassLoader(ClassLoader cl) {
    this.contextClassLoader = cl;
  }
  ...
}

contextClassLoader「線程上下文類加載器」,這究竟是什么東西?

首先 contextClassLoader 是那種需要顯示使用的類加載器,如果你沒有顯示使用它,也就永遠(yuǎn)不會在任何地方用到它。你可以使用下面這種方式來顯示使用它

Thread.currentThread().getContextClassLoader().loadClass(name);

這意味著如果你使用 forName(string name) 方法加載目標(biāo)類,它不會自動使用 contextClassLoader。那些因?yàn)榇a上的依賴關(guān)系而懶惰加載的類也不會自動使用 contextClassLoader來加載。

其次線程的 contextClassLoader 是從父線程那里繼承過來的,所謂父線程就是創(chuàng)建了當(dāng)前線程的線程。程序啟動時的 main 線程的 contextClassLoader 就是 AppClassLoader。這意味著如果沒有人工去設(shè)置,那么所有的線程的 contextClassLoader 都是 AppClassLoader。

那這個 contextClassLoader 究竟是做什么用的?我們要使用前面提到了類加載器分工與合作的原理來解釋它的用途。

它可以做到跨線程共享類,只要它們共享同一個 contextClassLoader。父子線程之間會自動傳遞 contextClassLoader,所以共享起來將是自動化的。

如果不同的線程使用不同的 contextClassLoader,那么不同的線程使用的類就可以隔離開來。

如果我們對業(yè)務(wù)進(jìn)行劃分,不同的業(yè)務(wù)使用不同的線程池,線程池內(nèi)部共享同一個 contextClassLoader,線程池之間使用不同的 contextClassLoader,就可以很好的起到隔離保護(hù)的作用,避免類版本沖突。

如果我們不去定制 contextClassLoader,那么所有的線程將會默認(rèn)使用 AppClassLoader,所有的類都將會是共享的。

線程的 contextClassLoader 使用場合比較罕見,如果上面的邏輯晦澀難懂也不必過于計(jì)較。

JDK9 增加了模塊功能之后對類加載器的結(jié)構(gòu)設(shè)計(jì)做了一定程度的修改,不過類加載器的原理還是類似的,作為類的容器,它起到類隔離的作用,同時還需要依靠雙親委派機(jī)制來建立不同的類加載器之間的合作關(guān)系。

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

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