什麼是ClassLoader

来源:https://www.cnblogs.com/weiqihome/archive/2018/12/05/10068542.html
-Advertisement-
Play Games

ClassLoader 做什麼的? 顧名思義,它是用來載入 Class 的。它負責將 Class 的位元組碼形式轉換成記憶體形式的 Class 對象。位元組碼可以來自於磁碟文件 *.class,也可以是 jar 包里的 *.class,也可以來自遠程伺服器提供的位元組流,位元組碼的本質就是一個位元組數組 []b ...


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 文件中載入,也可以從網路上不同的靜態文件伺服器來下載位元組碼再載入。

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

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 方法來動態載入驅動類。

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

鑽石依賴

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

我們平時使用的 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  
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言 開心一刻 著火了,他報警說:119嗎,我家發生火災了。 119問:在哪裡? 他說:在我家。 119問:具體點。 他說:在我家的廚房裡。 119問:我說你現在的位置。 他說:我趴在桌子底下。 119:我們怎樣才能到你家? 他說:你們不是有消防車嗎? 119說:燒死你個傻B算了。 路漫漫其修遠兮, ...
  • HashMap 和 Hashtable 是 Java 開發程式員必須要掌握的,也是在各種 Java 面試場合中必須會問到的。 但你對這兩者的區別瞭解有多少呢? 現在,棧長我給大家總結一下,或許有你不明朗的地方,在棧長的指點下都會撥開迷霧見晴天。 1、線程安全 Hashtable 是線程安全的,Has ...
  • A hard puzzle Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)Total Submission(s): 51690 Accepted Submission(s): 18916 ...
  • 一、快速冪原理 $$ 快速冪演算法,可以加快運算速度,使用快速冪演算法時間複雜度為O(logN) $$ $$ 以2^{50}為例 $$ ​ 在不使用數學函數的情況下,使用遍歷的方法,時間複雜度是O(N),需要遍歷50次對吧。 ​ 但是如果使用快速冪的話,那就快多了。具體是如何運算,先將50轉化成2進位數 ...
  • 依賴版本信息 Spring boot 2.1.0.RELEASE swagger2 2.7.0 1- mvn 配置 pom.xml 包引入 1 <!--swagger2依賴--> 2 <dependency> 3 <groupId>io.springfox</groupId> 4 <artifact ...
  • ArrayList底層使用時數組。LinkedList使用的是鏈表。 ArrayList: 數組查詢具有所有查詢特定元素比較快。而插入和刪除和修改比較慢(數組在記憶體中是一塊連續的記憶體,如果插入或刪除是需要移動記憶體)。 LinkedList: 鏈表不要求記憶體是連續的,在當前元素中存放下一個或上一個元素 ...
  • 題目內容: 你的程式要讀入一行文本,其中以空格分隔為若幹個單詞,以‘.’結束。你要輸出這行文本中每個單詞的長度。這裡的單詞與語言無關,可以包括各種符號,比如“it's”算一個單詞,長度為4。註意,行中可能出現連續的空格。 輸入格式: 輸入在一行中給出一行文本,以‘.’結束,結尾的句號不能計算在最後一 ...
  • 九大內置對象: 內置對象(又叫隱含對象),就是在jsp中,不需要創建(由伺服器<容器>來創建),可以直接使用的對象。 對象含義類作用域 request 請求對象 類型 javax.servlet.ServletRequest 作用域 Request response 響應對象 類型 javax.se ...
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...