什麼是ClassLoader

ClassLoader 作什麼的?

顧名思義,它是用來加載 Class 的。它負責將 Class 的字節碼形式轉換成內存形式的 Class 對象。字節碼能夠來自於磁盤文件 *.class,也能夠是 jar 包裏的 *.class,也能夠來自遠程服務器提供的字節流,字節碼的本質就是一個字節數組 []byte,它有特定的複雜的內部格式。

有不少字節碼加密技術就是依靠定製 ClassLoader 來實現的。先使用工具對字節碼文件進行加密,運行時使用定製的 ClassLoader 先解密文件內容再加載這些解密後的字節碼。
每一個 Class 對象的內部都有一個 classLoader 字段來標識本身是由哪一個 ClassLoader 加載的。ClassLoader 就像一個容器,裏面裝了不少已經加載的 Class 對象。
class Class<T> {
  ...
  private final ClassLoader classLoader;
  ...
}

 

延遲加載

JVM 運行並非一次性加載所須要的所有類的,它是按需加載,也就是延遲加載。程序在運行的過程當中會逐漸遇到不少不認識的新類,這時候就會調用 ClassLoader 來加載這些類。加載完成後就會將 Class 對象存在 ClassLoader 裏面,下次就不須要從新加載了。
 
好比你在調用某個類的靜態方法時,首先這個類確定是須要被加載的,可是並不會觸及這個類的實例字段,那麼實例字段的類別 Class 就能夠暫時沒必要去加載,可是它可能會加載靜態字段相關的類別,由於靜態方法會訪問靜態字段。而實例字段的類別須要等到你實例化對象的時候纔可能會加載。

各司其職

JVM 運行實例中會存在多個 ClassLoader,不一樣的 ClassLoader 會從不一樣的地方加載字節碼文件。它能夠從不一樣的文件目錄加載,也能夠從不一樣的 jar 文件中加載,也能夠從網絡上不一樣的靜態文件服務器來下載字節碼再加載。java

JVM 中內置了三個重要的 ClassLoader,分別是 BootstrapClassLoader、ExtensionClassLoader 和 AppClassLoader。mysql

BootstrapClassLoader 負責加載 JVM 運行時核心類,這些類位於 $JAVA_HOME/lib/rt.jar 文件中,咱們經常使用內置庫 java.xxx.* 都在裏面,好比 java.util. java.io.、java.nio. 、java.lang. 等等。這個 ClassLoader 比較特殊,它是由 C 代碼實現的,咱們將它稱之爲「根加載器」。
 
ExtensionClassLoader 負責加載 JVM 擴展類,好比 swing 系列、內置的 js 引擎、xml 解析器 等等,這些庫名一般以 javax 開頭,它們的 jar 包位於 $JAVA_HOME/lib/ext/*.jar 中,有不少 jar 包。
AppClassLoader 纔是直接面向咱們用戶的加載器,它會加載 Classpath 環境變量裏定義的路徑中的 jar 包和目錄。咱們本身編寫的代碼以及使用的第三方 jar 包一般都是由它來加載的。
 
那些位於網絡上靜態文件服務器提供的 jar 包和 class文件,jdk 內置了一個 URLClassLoader,用戶只須要傳遞規範的網絡路徑給構造器,就可使用 URLClassLoader 來加載遠程類庫了。URLClassLoader 不但能夠加載遠程類庫,還能夠加載本地路徑的類庫,取決於構造器中不一樣的地址形式。ExtensionClassLoader 和 AppClassLoader 都是 URLClassLoader 的子類,它們都是從本地文件系統里加載類庫。

AppClassLoader 能夠由 ClassLoader 類提供的靜態方法 getSystemClassLoader() 獲得,它就是咱們所說的「系統類加載器」,咱們用戶平時編寫的類代碼一般都是由它加載的。當咱們的 main 方法執行的時候,這第一個用戶類的加載器就是 AppClassLoader。

ClassLoader 傳遞性

程序在運行過程當中,遇到了一個未知的類,它會選擇哪一個 ClassLoader 來加載它呢?虛擬機的策略是使用調用者 Class 對象的 ClassLoader 來加載當前未知的類。何爲調用者 Class 對象?就是在遇到這個未知的類時,虛擬機確定正在運行一個方法調用(靜態方法或者實例方法),這個方法掛在哪一個類上面,那這個類就是調用者 Class 對象。前面咱們提到每一個 Class 對象裏面都有一個 classLoader 屬性記錄了當前的類是由誰來加載的。
由於 ClassLoader 的傳遞性,全部延遲加載的類都會由初始調用 main 方法的這個 ClassLoader 全全負責,它就是 AppClassLoader。

雙親委派

前面咱們提到 AppClassLoader 只負責加載 Classpath 下面的類庫,若是遇到沒有加載的系統類庫怎麼辦,AppClassLoader 必須將系統類庫的加載工做交給 BootstrapClassLoader 和 ExtensionClassLoader 來作,這就是咱們常說的「雙親委派」。


AppClassLoader 在加載一個未知的類名時,它並非當即去搜尋 Classpath,它會首先將這個類名稱交給 ExtensionClassLoader 來加載,若是 ExtensionClassLoader 能夠加載,那麼 AppClassLoader 就不用麻煩了。不然它就會搜索 Classpath 。
而 ExtensionClassLoader 在加載一個未知的類名時,它也並非當即搜尋 ext 路徑,它會首先將類名稱交給 BootstrapClassLoader 來加載,若是 BootstrapClassLoader 能夠加載,那麼 ExtensionClassLoader 也就不用麻煩了。不然它就會搜索 ext 路徑下的 jar 包。

這三個 ClassLoader 之間造成了級聯的父子關係,每一個 ClassLoader 都很懶,儘可能把工做交給父親作,父親幹不了了本身才會幹。每一個 ClassLoader 對象內部都會有一個 parent 屬性指向它的父加載器。
class ClassLoader {
  ...
  private final ClassLoader parent;
  ...
}

 

值得注意的是圖中的 ExtensionClassLoader 的 parent 指針畫了虛線,這是由於它的 parent 的值是 null,當 parent 字段是 null 時就表示它的父加載器是「根加載器」。若是某個 Class 對象的 classLoader 屬性值是 null,那麼就表示這個類也是「根加載器」加載的。注意這裏的 parent 不是 super 不是父類,只是 ClassLoader 內部的字段。

Class.forName

當咱們在使用 jdbc 驅動時,常常會使用 Class.forName 方法來動態加載驅動類。sql

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

其原理是 mysql 驅動的 Driver 類裏有一個靜態代碼塊,它會在 Driver 類被加載的時候執行。這個靜態代碼塊會將 mysql 驅動實例註冊到全局的 jdbc 驅動管理器裏。數組

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

forName 方法一樣也是使用調用者 Class 對象的 ClassLoader 來加載目標類。不過 forName 還提供了多參數版本,能夠指定使用哪一個 ClassLoader 來加載服務器

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

經過這種形式的 forName 方法能夠突破內置加載器的限制,經過使用自定類加載器容許咱們自由加載其它任意來源的類庫。根據 ClassLoader 的傳遞性,目標類庫傳遞引用到的其它類庫也將會使用自定義加載器加載。網絡

自定義加載器

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

loadClass() 方法是加載目標類的入口,它首先會查找當前 ClassLoader 以及它的雙親裏面是否已經加載了目標類,若是沒有找到就會讓雙親嘗試加載,若是雙親都加載不了,就會調用 findClass() 讓自定義加載器本身來加載目標類。ClassLoader 的 findClass() 方法是須要子類來覆蓋的,不一樣的加載器將使用不一樣的邏輯來獲取目標類的字節碼。拿到這個字節碼以後再調用 defineClass() 方法將字節碼轉換成 Class 對象。下面我使用僞代碼表示一下基本過程
class ClassLoader {

  // 加載入口,定義了雙親委派規則
  Class loadClass(String name) {
    // 是否已經加載了
    Class t = this.findFromLoaded(name);
    if(t == null) {
      // 交給雙親
      t = this.parent.loadClass(name)
    }
    if(t == null) {
      // 雙親都不行,只能靠本身了
      t = this.findClass(name);
    }
    return t;
  }
  
  // 交給子類本身去實現
  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) {
    // 尋找字節碼
    byte[] code = findCodeFromSomewhere(name);
    // 組裝Class對象
    return this.defineClass(code, name);
  }
}
自定義類加載器不易破壞雙親委派規則,不要輕易覆蓋 loadClass 方法。不然可能會致使自定義加載器沒法加載內置的核心類庫。在使用自定義加載器時,要明確好它的父加載器是誰,將父加載器經過子類的構造器傳入。若是父類加載器是 null,那就表示父加載器是「根加載器」。
// ClassLoader 構造器
protected ClassLoader(String name, ClassLoader parent);

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

Class.forName vs ClassLoader.loadClass

這兩個方法均可以用來加載目標類,它們之間有一個小小的區別,那就是 Class.forName() 方法能夠獲取原生類型的 Class,而 ClassLoader.loadClass() 則會報錯。工具

Class<?> x = Class.forName("[I");
System.out.println(x);

x = ClassLoader.getSystemClassLoader().loadClass("[I");
System.out.println(x);

---------------------
class [I

Exception in thread "main" java.lang.ClassNotFoundException: [I
...

鑽石依賴

項目管理上有一個著名的概念叫着「鑽石依賴」,是指軟件依賴致使同一個軟件包的兩個版本須要共存而不能衝突。post

咱們平時使用的 maven 是這樣解決鑽石依賴的,它會從多個衝突的版本中選擇一個來使用,若是不一樣的版本之間兼容性很糟糕,那麼程序將沒法正常編譯運行。Maven 這種形式叫「扁平化」依賴管理。

使用 ClassLoader 能夠解決鑽石依賴問題。不一樣版本的軟件包使用不一樣的 ClassLoader 來加載, 位於不一樣 ClassLoader 中名稱同樣的類其實是不一樣的類。下面讓咱們使用 URLClassLoader 來嘗試一個簡單的例子,它默認的父加載器是 AppClassLoader

$ cat ~/source/jcl/v1/Dep.java
public class Dep {
	public void print() {
		System.out.println("v1");
	}
}

$ cat ~/source/jcl/v2/Dep.java
public class Dep {
 public void print() {
  System.out.println("v1");
 }
}

$ cat ~/source/jcl/Test.java
public class Test {
	public static void main(String[] args) throws Exception {
		String v1dir = "file:///Users/qianwp/source/jcl/v1/";
		String v2dir = "file:///Users/qianwp/source/jcl/v2/";
		URLClassLoader v1 = new URLClassLoader(new URL[]{new URL(v1dir)});
		URLClassLoader v2 = new URLClassLoader(new URL[]{new URL(v2dir)});
		
  Class<?> depv1Class = v1.loadClass("Dep");
		Object depv1 = depv1Class.getConstructor().newInstance();
		depv1Class.getMethod("print").invoke(depv1);

		Class<?> depv2Class = v2.loadClass("Dep");
		Object depv2 = depv2Class.getConstructor().newInstance();
		depv2Class.getMethod("print").invoke(depv2);
	 
  System.out.println(depv1Class.equals(depv2Class));
 }
}

在運行以前,咱們須要對依賴的類庫進行編譯

$ cd ~/source/jcl/v1
$ javac Dep.java
$ cd ~/source/jcl/v2
$ javac Dep.java
$ cd ~/source/jcl
$ javac Test.java
$ java Test
v1
v2
false

在這個例子中若是兩個 URLClassLoader 指向的路徑是同樣的,下面這個表達式仍是 false,由於即便是一樣的字節碼用不一樣的 ClassLoader 加載出來的類都不能算同一個類

depv1Class.equals(depv2Class)

咱們還可讓兩個不一樣版本的 Dep 類實現同一個接口,這樣能夠避免使用反射的方式來調用 Dep 類裏面的方法。

Class<?> depv1Class = v1.loadClass("Dep");
IPrint depv1 = (IPrint)depv1Class.getConstructor().newInstance();
depv1.print()
ClassLoader 當然能夠解決依賴衝突問題,不過它也限制了不一樣軟件包的操做界面必須使用反射或接口的方式進行動態調用。Maven 沒有這種限制,它依賴於虛擬機的默認懶惰加載策略,運行過程當中若是沒有顯示使用定製的 ClassLoader,那麼從頭至尾都是在使用 AppClassLoader,而不一樣版本的同名類必須使用不一樣的 ClassLoader 加載,因此 Maven 不能完美解決鑽石依賴。 若是你想知道有沒有開源的包管理工具能夠解決鑽石依賴的,我推薦你瞭解一下 sofa-ark,它是螞蟻金服開源的輕量級類隔離框架。

分工與合做

這裏咱們從新理解一下 ClassLoader 的意義,它至關於類的命名空間,起到了類隔離的做用。位於同一個 ClassLoader 裏面的類名是惟一的,不一樣的 ClassLoader 能夠持有同名的類。ClassLoader 是類名稱的容器,是類的沙箱。

不一樣的 ClassLoader 之間也會有合做,它們之間的合做是經過 parent 屬性和雙親委派機制來完成的。parent 具備更高的加載優先級。除此以外,parent 還表達了一種共享關係,當多個子 ClassLoader 共享同一個 parent 時,那麼這個 parent 裏面包含的類能夠認爲是全部子 ClassLoader 共享的。這也是爲何 BootstrapClassLoader 被全部的類加載器視爲祖先加載器,JVM 核心類庫天然應該被共享。

Thread.contextClassLoader

若是你稍微閱讀過 Thread 的源代碼,你會在它的實例字段中發現有一個字段很是特別
class Thread {
  ...
  private ClassLoader contextClassLoader;
  
  public ClassLoader getContextClassLoader() {
    return contextClassLoader;
  }
  
  public void setContextClassLoader(ClassLoader cl) {
    this.contextClassLoader = cl;
  }
  ...
}

contextClassLoader「線程上下文類加載器」,這到底是什麼東西?

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

這意味着若是你使用 forName(string name) 方法加載目標類,它不會自動使用 contextClassLoader。那些由於代碼上的依賴關係而懶惰加載的類也不會自動使用 contextClassLoader來加載。

其次線程的 contextClassLoader 默認是從父線程那裏繼承過來的,所謂父線程就是建立了當前線程的線程。程序啓動時的 main 線程的 contextClassLoader 就是 AppClassLoader。這意味着若是沒有人工去設置,那麼全部的線程的 contextClassLoader 都是 AppClassLoader。
那這個 contextClassLoader 到底是作什麼用的?咱們要使用前面提到了類加載器分工與合做的原理來解釋它的用途。
 
它能夠作到跨線程共享類,只要它們共享同一個 contextClassLoader。父子線程之間會自動傳遞 contextClassLoader,因此共享起來將是自動化的。
 
若是不一樣的線程使用不一樣的 contextClassLoader,那麼不一樣的線程使用的類就能夠隔離開來。
 
若是咱們對業務進行劃分,不一樣的業務使用不一樣的線程池,線程池內部共享同一個 contextClassLoader,線程池之間使用不一樣的 contextClassLoader,就能夠很好的起到隔離保護的做用,避免類版本衝突。
 
若是咱們不去定製 contextClassLoader,那麼全部的線程將會默認使用 AppClassLoader,全部的類都將會是共享的。
 
線程的 contextClassLoader 使用場合比較罕見,若是上面的邏輯晦澀難懂也沒必要過於計較。
 
JDK9 增長了模塊功能以後對類加載器的結構設計作了必定程度的修改,不過類加載器的原理仍是相似的,做爲類的容器,它起到類隔離的做用,同時還須要依靠雙親委派機制來創建不一樣的類加載器之間的合做關係。
 
轉載連接:https://juejin.im/post/5c04892351882516e70dcc9b
相關文章
相關標籤/搜索