1. 程式人生 > >吃透類載入(下)

吃透類載入(下)

前言

在前面吃透類載入(上)大致介紹ClassLoader的特性已經雙親委派機制,下面再深入瞭解其他特性。

Class.forName

當我們在使用 jdbc 驅動時,經常會使用 Class.forName 方法來動態載入驅動類。

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);

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

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
...

分工與合作 

這裡我們重新理解一下 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「執行緒上下文類載入器」,這究竟是什麼東西?

首先 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,所有的類都將會是共享的。

原文來源

https://toutiao.io/u/445827