一文帶你深扒ClassLoader內核,揭開它的神秘面紗!

2020-12-16 楊建榮的資料庫筆記

本文轉載自 我沒有三顆心臟 作者:我沒有三顆心臟 id: wmyskxz

「MoreThanJava」宣揚的是 「學習,不止 CODE」

如果覺得 「不錯」的朋友,歡迎 「關注 + 留言 + 分享」,文末有完整的獲取連結,您的支持是我前進的最大的動力!

前言

ClassLoader 可以說是 Java 最為神秘的功能之一了,好像大家都知道怎麼回事兒 ,又都說不清楚具體是怎麼一回事 。

今天,我們就來深度扒一扒,揭開它神秘的面紗!

Part 1. 類加載是做什麼的?

首先,我們知道,Java 為了實現 「一次編譯,到處運行」的目標,採用了一種特別的方案:先 編譯與任何具體及其環境及作業系統環境無關的中間代碼(也就是 .class 字節碼文件),然後交由各個平臺特定的 Java 解釋器(也就是 JVM)來負責 解釋 運行。

ClassLoader 就是那個把字節碼交給 JVM 的搬運工 。它負責將 字節碼形式的 Class 轉換成 JVM 中 內存形式 的 Class 對象。

字節碼可以是來自於磁碟上的 .class文件,也可以是 jar 包裡的 *.class,甚至是來自遠程伺服器提供的字節流。字節碼的本質其實就是一個有特定複雜格式的字節數組 byte[]

另外,類加載器不光可以把 Class 加載到 JVM 之中並解析成 JVM 統一要求的對象格式,還有一個重要的作用就是 審查每個類應該由誰加載

而且,這些 Java 類不會一次全部加載到內存,而是在應用程式需要時加載,這也是需要類加載器的地方。

Part 2. ClassLoader 類結構分析

以下就是 ClassLoader 的主要方法了:

defineClass()用於將 byte 字節流解析成 JVM 能夠識別的 Class 對象。有了這個方法意味著我們不僅可以通過 .class 文件實例化對象,還可以通過其他方式實例化對象,例如通過網絡接收到一個類的字節碼。

findClass()通常和 defineClass() 一起使用,我們需要直接覆蓋 ClassLoader 父類的 findClass() 方法來實現類的加載規則,從而取得要加載類的字節碼。

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

如果你不想重新定義加載類的規則,也沒有複雜的處理邏輯,只想在運行時能夠加載自己制定的一個類,那麼你可以用 this.getClass().getClassLoader().loadClass("class")調用 ClassLoader 的 loadClass() 方法來獲取這個類的 Class 對象,這個 loadClass() 還有重載方法,你同樣可以決定再什麼時候解析這個類。

loadClass()用於接受一個全類名,然後返回一個 Class 類型的對象。

resolveClass()用於對 Class 進行 連結,也就是把單一的 Class 加入到有繼承關係的類樹中。如果你想在類被加載到 JVM 中時就被連結(Link),那麼可以在調用 defineClass() 之後緊接著調用一個 resolveClass() 方法,當然你也可以選擇讓 JVM 來解決什麼時候才連結這個類(通常是真正被實實例化的時候)。

ClassLoader 是個抽象類,它還有很多子類,如果我們要實現自己的 ClassLoader,一般都會繼承 URLClassLoader這個子類,因為這個類已經幫我們實現了大部分工作。

例如,我們來看一下 java.net.URLClassLoader.findClass()方法的實現:

// 入參為 Class 的 binary name,如 java.lang.Stringprotected Class<?> findClass(final String name) throws ClassNotFoundException { // 以上代碼省略 // 通過 binary name 生成包路徑,如 java.lang.String -> java/lang/String.class String path = name.replace('.', '/').concat(".class"); // 根據包路徑,找到該 Class 的文件資源 Resource res = ucp.getResource(path, false); if (res != null) { try { // 調用 defineClass 生成 java.lang.Class 對象 return defineClass(name, res); } catch (IOException e) { throw new ClassNotFoundException(name, e); } } else { return null; } // 以下代碼省略}

Part 3. Java 類加載流程詳解

以下就是 ClassLoader 加載一個 class 文件到 JVM 時需要經過的步驟。

事實上,我們每一次在 IDEA 中點擊運行時,IDE 都會默認替我們執行以下的命令:

javac Xxxx.java 找到源文件中的 public class,再找 public class 引用的其他類,Java 編譯器會根據每一個類生成一個字節碼文件;

java Xxxx 找到文件中的唯一主類 public class,並根據 public static 關鍵字找到跟主類關聯可執行的 main 方法 ,開始執行。

在真正的運行 main方法之前,JVM 需要 加載、連結 以及 初始化 上述的 Xxxx 類。

這一步是讀取到類文件產生的二進位流(findClass()),並轉換為特定的數據結構(defineClass()),初步校驗 cafe babe魔法數 、常量池、文件長度、是否有父類等,然後在 Java 中創建對應類的 java.lang.Class 實例,類中存儲的各部分信息也需要對應放入 運行時數據區 中(例如靜態變量、類信息等放入方法區)。

以下是一個 Class 文件具有的基本結構的簡單圖示:

如果對 Class 文件更多細節感興趣的可以進一步閱讀:https://juejin.im/post/6844904199617003528

這裡我們可能會有一個疑問,為什麼 JVM 允許還沒有進行驗證、準備和解析的類信息放入方法區呢?

答案是加載階段和連結階段的部分動作(比如一部分字節碼文件格式驗證動作)是 交叉進行的,也就是說 加載階段還沒完成,連結階段可能已經開始了。但這些夾雜在加載階段的動作(驗證文件格式等)仍然屬於連結操作。

Link 階段包括驗證、準備、解析三個步驟。下面我們來詳細說說。

驗證是連接階段的第一步,這一階段的目的是 為了確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。驗證階段大致會完成 4個階段的檢驗動作:

文件格式驗證:驗證字節流是否符合 Class 文件格式的規範;例如:是否以 0xCAFEBABE 開頭、主次版本號是否在當前虛擬機的處理範圍之內、常量池中的常量是否有不被支持的類型。

元數據驗證:對字節碼描述的信息進行語義分析(注意:對比 javac 編譯階段的語義分析),以保證其描述的信息符合 Java 語言規範的要求;例如:這個類是否有父類,除了 java.lang.Object 之外。

字節碼驗證:通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。

符號引用驗證:確保解析動作能正確執行。

驗證階段是非常重要的,但不是必須的,它對程序運行期沒有影響,如果所引用的類經過反覆驗證,那麼可以考慮採用 -Xverifynone參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。

準備階段是正式為類變量分配內存並設置類變量初始值的階段,這些內存都將在 方法區中分配。對於該階段有以下幾點需要注意:

1 這時候進行內存分配的 僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨著對象一塊分配在 Java 堆中。

2 這裡所設置的 初始值通常情況下是數據類型默認的零值(如 00Lnullfalse等),而不是被在 Java 代碼中被顯式地賦予的值。

3 如果類欄位的欄位屬性表中存在 ConstantValue 屬性,即 同時被 finalstatic 修飾,那麼在準備階段變量 value 就會被初始化為 ConstValue 屬性所指定的值。

例如,假設這裡有一個類變量 public static int value = 666;,在準備階段時初始值是 0而不是 666,在 初始化階段 才會被真正賦值為 666

假設是一個靜態類變量 public static final int value = 666;,則再準備階段 JVM 就已經賦值為 666了。

解析階段是虛擬機將常量池內的 符號引用替換為 直接引用 的過程,解析動作主要針對類或接口、欄位、類方法、接口方法、方法類型、方法句柄和調用點限定符 7 類符號引用進行。

符號引用的作用是在編譯的過程中,JVM 並不知道引用的具體地址,所以用符號引用進行代替,而在解析階段將會將這個符號引用轉換為真正的內存地址。

直接引用可以理解為指向 類、變量、方法 的指針,指向 實例 的指針和一個 間接定位 到對象的對象句柄。

為了理解上面兩種概念的區別,來看一個實際的例子吧:

public class Tester { public static void main(String[] args) { String str = "關注【我沒有三顆心臟】,關注更多精彩"; System.out.println(str); }}

我們先在該類同級目錄下運行 javac Tester編譯成 .class 文件然後再利用 javap -verbose Tester 查看類的詳細信息 :

// 上面是類的詳細信息省略...{ // ..... public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=1 0: ldc #7 // String 關注【我沒有三顆心臟】,關注更多精彩 2: astore_1 3: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream; 6: aload_1 7: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 10: return LineNumberTable: line 4: 0 line 5: 3 line 6: 10}SourceFile: "Tester.java"

可以看到,上面定義的 str變量在編譯階段會被解析稱為 符號引用,符號引用的標誌是 astore_<n>,這裡就是 astore_1

store_1的含義是將操作數棧頂的 關注【我沒有三顆心臟】,關注更多精彩保存回索引為 1 的局部變量表中,此時訪問變量 str 就會讀取局部變量表索引值為 1 中的數據。所以局部變量 str 就是一個符號引用。

再來看另外一個例子:

public class Tester { public static void main(String[] args) { System.out.println("關注【我沒有三顆心臟】,關注更多精彩"); }}

這一段代碼反編譯之後得到如下的代碼:

// 上面是類的詳細信息省略...{ // ...... public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #13 // String 關注【我沒有三顆心臟】,關注更多精彩 5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 4: 0 line 5: 8}SourceFile: "Tester.java"

我們可以看到這裡直接使用了 ldc指令將 關注【我沒有三顆心臟】,關注更多精彩 推送到了棧,緊接著就是調用指令 invokevirtual,並沒有將字符串存入局部變量表中,這裡的字符串就是一個 直接引用

初始化,為類的靜態變量賦予正確的初始值,JVM 負責對類進行初始化,主要對類變量進行初始化。在 Java 中對類變量進行初始值設定有兩種方式:

1 聲明類變量是指定初始值;

2 使用靜態代碼塊為類變量指定初始值;

JVM 初始化步驟:

1 假如這個類還沒有被加載和連接,則程序先加載並連接該類

2 假如該類的直接父類還沒有被初始化,則先初始化其直接父類

3 假如類中有初始化語句,則系統依次執行這些初始化語句

類初始化時機:只有當對類的主動使用的時候才會導致類的初始化,類的主動使用包括以下幾種:

創建類的實例,也就是 new的方式

訪問某個類或接口的靜態變量,或者對該靜態變量賦值

調用類的靜態方法

反射(如 Class.forName("com.wmyskxz.Tester")

初始化某個類的子類,則其父類也會被初始化

Java 虛擬機啟動時被標明為啟動類的類,直接使用 java.exe命令來運行某個主類

使用 JDK 7 新加入的動態語言支持時,如果一個 java.lang.invoke.MethodHanlde實例最後的解析結果為 REF_getstaticREF_putstaticREF_invokeStaticREF_newInvokeSpecial 四種類型的方法句柄時,都需要先初始化該句柄對應的類

接口中定義了 JDK 8 新加入的默認方法(default修飾符),實現類在初始化之前需要先初始化其接口

Part 4. 深入理解雙親委派模型

我們在上面已經了解了一個類是如何被加載進 JVM 的——依靠類加載器——在 Java 語言中自帶有三個類加載器:

Bootstrap ClassLoader最頂層的加載類,主要加載 核心類庫%JRE_HOME%lib 下的rt.jarresources.jarcharsets.jarclass 等。

Extention ClassLoader擴展的類加載器,加載目錄 %JRE_HOME%libext 目錄下的 jar 包和 class 文件。

Appclass Loader 也稱為 SystemAppClass加載當前應用的 classpath 的所有類。

我們可以通過一個簡單的例子來簡單了解 Java 中這些自帶的類加載器:

public class PrintClassLoader { public static void main(String[] args) { printClassLoaders(); } public static void printClassLoaders() { System.out.println("Classloader of this class:" + PrintClassLoader.class.getClassLoader()); System.out.println("Classloader of Logging:" + com.sun.javafx.util.Logging.class.getClassLoader()); System.out.println("Classloader of ArrayList:" + java.util.ArrayList.class.getClassLoader()); }}

上方程序列印輸出如下:

Classloader of this class:sun.misc.Launcher$AppClassLoader@18b4aac2Classloader of Logging:sun.misc.Launcher$ExtClassLoader@60e53b93Classloader of ArrayList:null

如我們所見,這裡分別對應三種不同類型的類加載器:AppClassLoader、ExtClassLoader 和 BootstrapClassLoader(顯示為 null)。

一個很好的問題是:Java 類是由 java.lang.ClassLoader實例加載的,但類加載器本身也是類,那麼誰來加載類加載器呢?

我們假裝不知道,先來跟著源碼一步一步來看。

在 JDK 源碼 sun.misc.Launcher中,蘊含了 Java 虛擬機的入口方法:

public class Launcher { private static Launcher launcher = new Launcher(); private static String bootClassPath = System.getProperty("sun.boot.class.path"); public static Launcher getLauncher() { return launcher; } private ClassLoader loader; public Launcher() { // Create the extension class loader ClassLoader extcl; try { extcl = ExtClassLoader.getExtClassLoader(); } catch (IOException e) { throw new InternalError( "Could not create extension class loader", e); } // Now create the class loader to use to launch the application try { loader = AppClassLoader.getAppClassLoader(extcl); } catch (IOException e) { throw new InternalError( "Could not create application class loader", e); } // 設置 AppClassLoader 為線程上下文類加載器,這個文章後面部分講解 Thread.currentThread().setContextClassLoader(loader); } /* * Returns the class loader used to launch the main application. */ public ClassLoader getClassLoader() { return loader; } /* * The class loader used for loading installed extensions. */ static class ExtClassLoader extends URLClassLoader {} /** * The class loader used for loading from java.class.path. * runs in a restricted security context. */ static class AppClassLoader extends URLClassLoader {}}

源碼有精簡,但是我們可以得到以下信息:

1 Launcher 初始化了 ExtClassLoader 和 AppClassLoader。

2 Launcher 沒有看到 Bootstrap ClassLoader 的影子,但是有一個叫做 bootClassPath的變量,大膽一猜就是 Bootstrap ClassLoader 加載的 jar 包的路徑。

3 ExtClassLoader 和 AppClassLoader 都繼承自 URLClassLoader,進一步查看 ClassLoader 的繼承樹,傳說中的雙親委派模型也並沒有出現。

4 注意以下代碼:

ClassLoader extcl; extcl = ExtClassLoader.getExtClassLoader();loader = AppClassLoader.getAppClassLoader(extcl);

分別跟蹤查看到這兩個 ClassLoader 初始化時的代碼:

// 一直追蹤到最頂層的 ClassLoader 定義,構造器的第二個參數標識了類加載器的父類private ClassLoader(Void unused, ClassLoader parent) { this.parent = parent; // 代碼省略.....}// Ext 設置自己的父類為 nullpublic ExtClassLoader(File[] var1) throws IOException { super(getExtURLs(var1), (ClassLoader)null, Launcher.factory); SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);}// 手動把 Ext 設置為 App 的 parent(這裡的 var2 是傳進來的 extc1)AppClassLoader(URL[] var1, ClassLoader var2) { super(var1, var2, Launcher.factory); this.ucp.initLookupCache(this);}

由此,我們得到了這樣一個類加載器的關係圖:

奇怪,為什麼 ExtClassLoader 的 parent明明是 null,我們卻一般地認為 Bootstrap ClassLoader 才是 ExtClassLoader 的父加載器呢?

答案的一部分就藏在 java.lang.ClassLoader.loadClass()方法裡面:(這也就是著名的「雙親委派模型」現場了)

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ synchronized (getClassLoadingLock(name)) { // 首先檢查是否已經加載過了 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { // 父加載器不為空則調用父加載器的 loadClass 方法 c = parent.loadClass(name, false); } else { // 父加載器為空則調用 Bootstrap ClassLoader c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); // 父加載器沒有找到,則調用 findclass c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { // 調用 resolveClass() resolveClass(c); } return c; }}

代碼邏輯很好地解釋了雙親委派的原理。

1 當前 ClassLoader 首先從 自己已經加載的類中查詢是否此類已經加載,如果已經加載則直接返回原來已經加載的類。(每個類加載器都有自己的加載緩存,當一個類被加載了以後就會放入緩存,等下次加載的時候就可以直接返回了。)

2 當前 ClassLoader 的緩存中沒有找到被加載的類的時候,委託父類加載器去加載,父類加載器採用同樣的策略,首先查看自己的緩存,然後委託父類的父類去加載,一直到 Bootstrap ClassLoader。(當所有的父類加載器都沒有加載的時候,再由當前的類加載器加載,並將其放入它自己的緩存中,以便下次有加載請求的時候直接返回。)

所以,答案的另一部分是因為最高一層的類加載器 Bootstrap 是通過 C/C++ 實現的,並不存在於 JVM 體系內 ,所以輸出為 null

OK,我們理解了為什麼 ExtClassLoader 的父加載器為什麼是表示為 null的 Bootstrap 加載器,那我們 自己實現的 ClassLoader 父加載器應該是誰呢?

觀察一下 ClassLoader 的源碼就知道了:

protected ClassLoader(ClassLoader parent) { this(checkCreateClassLoader(), parent);}protected ClassLoader() { this(checkCreateClassLoader(), getSystemClassLoader());}

類加載器的 parent的賦值是在 ClassLoader 對象的構造方法中,它有兩個情況:

1 由外部類創建 ClassLoader 時直接指定一個 ClassLoader 為 parent

2 由 getSystemClassLoader()方法生成,也就是在 sun.misc.Laucher 通過 getClassLoader() 獲取,也就是 AppClassLoader。直白的說,一個 ClassLoader 創建時如果沒有指定 parent,那麼它的 parent 默認就是 AppClassLoader。(建議去看一下源碼)

簡單來說,主要是為了 安全性,避免用戶自己編寫的類動態替換 Java 的一些核心類,比如 String,同時也 避免了重複加載,因為 JVM 中區分不同類,不僅僅是根據類名,相同的 class 文件被不同的 ClassLoader 加載就是不同的兩個類,如果相互轉型的話會拋 java.lang.ClassCaseException

如果我們要實現自己的類加載器,不管你是直接實現抽象類 ClassLoader,還是繼承 URLClassLoader 類,或者其他子類,它的父加載器都是 AppClassLoader。

因為不管調用哪個父類構造器,創建的對象都必須最終調用 getSystemClassLoader()作為父加載器 。而該方法最終獲取到的正是 AppClassLoader 。

這也就是我們熟知的最終的雙親委派模型了。

Part 5. 實現自己的類加載器

在學習了類加載器的實現機制之後,我們知道了雙親委派模型並非強制模型,用戶可以自定義類加載器,在什麼情況下需要自定義類加載器呢?

1 隔離加載類。在某些框架內進行中間件與應用的模塊隔離,把類加載器到不同的環境。比如,阿里內某容器框架通過自定義類加載器確保應用中依賴的 jar包不會影響到中間件運行時使用的 jar 包。

2 修改類加載方式。類的加載模型並非強制,除了 Bootstrap 外,其他的加載並非一定要引入,或者根據實際情況在某個時間點進行按需的動態加載。

3 擴展加載源。比如從資料庫、網絡,甚至是電視機頂盒進行加載。(下面我們會編寫一個從網絡加載類的例子)

4 防止源碼洩露。Java 代碼容易被編譯和篡改,可以進行編譯加密。那麼類加載器也需要自定義,還原加密的字節碼。

實現一個自定義的類加載器比較簡單:繼承 ClassLoader,重寫 findClass()方法,調用 defineClass() 方法,就差不多行了。

我們先來編寫一個測試用的類文件:

public class Tester { public void say() { System.out.println("關注【我沒有三顆心臟】,解鎖更多精彩!"); }}

在同級目錄下執行 javac Tester.java命令,並把編譯後的 Tester.class 放到指定的目錄下(我這邊為了方便就放在桌面上啦 /Users/wmyskxz/Desktop

我們編寫自定義 ClassLoader 代碼:

import java.io.ByteArrayOutputStream;import java.io.File;import java.io.FileInputStream;import java.io.IOException;public class MyClassLoader extends ClassLoader { private final String mLibPath; public MyClassLoader(String path) { // TODO Auto-generated constructor stub mLibPath = path; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { // TODO Auto-generated method stub String fileName = getFileName(name); File file = new File(mLibPath, fileName); try { FileInputStream is = new FileInputStream(file); ByteArrayOutputStream bos = new ByteArrayOutputStream(); int len = 0; try { while ((len = is.read()) != -1) { bos.write(len); } } catch (IOException e) { e.printStackTrace(); } byte[] data = bos.toByteArray(); is.close(); bos.close(); return defineClass(name, data, 0, data.length); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } return super.findClass(name); } // 獲取要加載的 class 文件名 private String getFileName(String name) { // TODO Auto-generated method stub int index = name.lastIndexOf('.'); if (index == -1) { return name + ".class"; } else { return name.substring(index + 1) + ".class"; } }}

我們在 findClass()方法中定義了查找 class 的方法,然後數據通過 defineClass() 生成了 Class 對象。

我們需要刪除剛才在項目目錄創建的 Tester.java和編譯後的 Tester.class 文件來觀察效果:

import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;public class ClassLoaderTester { public static void main(String[] args) { // 創建自定義的 ClassLoader 對象 MyClassLoader myClassLoader = new MyClassLoader("/Users/wmyskxz/Desktop"); try { // 加載class文件 Class<?> c = myClassLoader.loadClass("Tester"); if(c != null){ try { Object obj = c.newInstance(); Method method = c.getDeclaredMethod("say",null); //通過反射調用Test類的say方法 method.invoke(obj, null); } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | SecurityException | IllegalArgumentException | InvocationTargetException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } }}

運行測試,正常輸出:

關注【我沒有三顆心臟】,解鎖更多精彩!

突破了 JDK 系統內置加載路徑的限制之後,我們就可以編寫自定義的 ClassLoader。你完全可以按照自己的意願進行業務的定製,將 ClassLoader 玩出花樣來。

例如,一個加密解密的類加載器。(不涉及完整代碼,我們可以來說一下思路和關鍵代碼)

首先,在編譯之後的字節碼文件中動一動手腳,例如,給文件每一個 byte異或一個數字 2:(這就算是模擬加密過程)

File file = new File(path);try { FileInputStream fis = new FileInputStream(file); FileOutputStream fos = new FileOutputStream(path+"en"); int b = 0; int b1 = 0; try { while((b = fis.read()) != -1){ // 每一個 byte 異或一個數字 2 fos.write(b ^ 2); } fos.close(); fis.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); }} catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace();}

然後我們再在 findClass()中自己解密:

File file = new File(mLibPath,fileName);try { FileInputStream is = new FileInputStream(file); ByteArrayOutputStream bos = new ByteArrayOutputStream(); int len = 0; byte b = 0; try { while ((len = is.read()) != -1) { // 將數據異或一個數字 2 進行解密 b = (byte) (len ^ 2); bos.write(b); } } catch (IOException e) { e.printStackTrace(); } byte[] data = bos.toByteArray(); is.close(); bos.close(); return defineClass(name,data,0,data.length);} catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace();}

其實非常類似,也不做過多講解,直接上代碼:

import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.net.URL; public class NetworkClassLoader extends ClassLoader { private String rootUrl; public NetworkClassLoader(String rootUrl) { // 指定URL this.rootUrl = rootUrl; } // 獲取類的字節碼 @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = getClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else { return defineClass(name, classData, 0, classData.length); } } private byte[] getClassData(String className) { // 從網絡上讀取的類的字節 String path = classNameToPath(className); try { URL url = new URL(path); InputStream ins = url.openStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int bufferSize = 4096; byte[] buffer = new byte[bufferSize]; int bytesNumRead = 0; // 讀取類文件的字節 while ((bytesNumRead = ins.read(buffer)) != -1) { baos.write(buffer, 0, bytesNumRead); } return baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); } return null; } private String classNameToPath(String className) { // 得到類文件的URL return rootUrl + "/" + className.replace('.', '/') + ".class"; } }

Part 6. 必要的擴展閱讀

學習到這裡,我們對 ClassLoader 已經不再陌生了,但是仍然有一些必要的知識點需要去掌握 ,希望您能認真閱讀以下的材料:

1 能不能自己寫一個類叫 java.lang.System或者 java.lang.String - https://blog.csdn.net/tang9140/article/details/42738433

2 深入理解 Java 之 JVM 啟動流程 - https://cloud.tencent.com/developer/article/1038435

3 真正理解線程上下文類加載器(多案例分析)- https://blog.csdn.net/yangcheng33/article/details/52631940

4 曹工雜談:Java 類加載器還會死鎖?這是什麼情況?- https://www.cnblogs.com/grey-wolf/p/11378747.html#_label2

5 謹防JDK8重複類定義造成的內存洩漏 - https://segmentfault.com/a/1190000022837543

7 Tomcat 類加載器的實現- https://juejin.im/post/6844903945496690695

8 Spring 中的類加載機制- https://www.shuzhiduo.com/A/gVdnwgAlzW/

參考資料

《深入分析 Java Web 技術內幕》 | 許令波 著

Java 類加載機制分析 - https://www.jianshu.com/p/3615403c7c84

Class 文件解析實戰 - https://juejin.im/post/6844904199617003528

圖文兼備看懂類加載機制的各個階段,就差你了!- https://juejin.im/post/6844904119258316814

Java面試知識點解析(三)——JVM篇 - https://www.wmyskxz.com/2018/05/16/java-mian-shi-zhi-shi-dian-jie-xi-san-jvm-pian/

一看你就懂,超詳細Java中的ClassLoader詳解 - https://blog.csdn.net/briblue/article/details/54973413

相關焦點

  • 你看的真是超高清?揭開原生4K的神秘面紗
    你看的真是超高清?揭開原生4K的神秘面紗究竟什麼是4K?      首先我們要明確「4K」的概念,其實4K解析度並不是指某個特定數值,它指的是水平方向每行達到或者接近4096個的像素點,而在不同的應用領域,4K解析度也有所不同。
  • 揭開秋葵的神秘面紗
    (原標題:揭開秋葵的神秘面紗) 華龍網11月30日10時訊 這段時間
  • 《探秘冬蟲夏草》——揭開冬蟲夏草的神秘面紗
    《探秘冬蟲夏草》——揭開冬蟲夏草的神秘面紗 2020-12-11 12:44 來源:澎湃新聞·澎湃號·政務
  • 帶你揭開石墨的神秘面紗
    你所熟知的石墨,大概就是黑黑的、軟硬適中的鉛筆芯了,哦對了,高中物理也告訴過你電池裡面也有石墨。其實,早在3 000年多年前商代就有用石墨書寫的文字。中國發現和利用石墨的歷史悠久,《水經注》就載有「洛水側有石墨山。
  • 揭開神秘面紗,原來你是這樣的梵淨山
    揭開神秘面紗,原來你是這樣的梵淨山 2020-12-03 09:54 來源:澎湃新聞 政務
  • 科普動畫 | 一起揭開臭氧的神秘面紗~
    科普動畫 | 一起揭開臭氧的神秘面紗~ 2020-07-08 18:19 來源:澎湃新聞·澎湃號·政務
  • 胡夫金字塔「皇后墓室」神秘通道今天揭開神秘面紗
    胡夫金字塔「皇后墓室」神秘通道今天揭開神秘面紗 2002年9月17日01:45  北京青年報   本報記者報導 吊足了全球觀眾胃口的胡夫金字塔「皇后墓室」神秘通道將在今天揭開面紗。
  • 惠人原汁機怎麼樣,為你揭開它的神秘面紗
    但事實上,生活從來都是不快也不慢,快節奏的只是生活裡的你我,生活本就應該慢下來好好享受。這兩年,原汁機成為了很多人的健康選擇,但原汁機品牌那麼多,究竟該如何挑選呢?這時,首創原汁機的惠人就引起了不少人的關注。那麼究竟惠人原汁機怎麼樣,下面小編就來為你揭開它的神秘面紗。
  • 揭開「散爆新品」神秘面紗,今冬育碧或成最大贏家?
    除此之外,在新品發布環節更是爆點不斷,公布了大量未公開內容以及神秘的重磅消息。  2015年10月,散爆網絡科技有限公司在上海成立,其主要作品為二次元戰旗策略手遊《少女前線》。少前製作人羽中早在2013年就以「雲母組」的名義發售《麵包房少女》。此後製作班底不斷壯大,羽中的帶領下潛心研發出了硬核戰術策略手遊《少女前線》。
  • 揭開SAP中國研究院的神秘面紗
    筆者走進了SAP中國研究院,隨著SAP專家的分析,揭開了SAP中國研究院神秘的面紗。  ▲體驗雲計算-走進SAP中國研究院全體合影紀念  合影后,來自SAP企業傳播部的相關工作人員,詳細的帶領大家參觀了SAP中國研究院,揭開了SAP中國研究院的神秘面紗。
  • 《蜘蛛俠2》早期概念圖,神秘客揭開面紗後,你看是誰變的?
    不過關於漫威MCU第三階段的電影的討論熱度依然不減,甚至還不斷曝出了一系列的早期概念圖,那麼接下來我們就一起來看看一組《蜘蛛俠:英雄遠徵》的概念圖吧。回顧一下《蜘蛛俠:英雄遠徵》這部電影的故事劇情,在電影中,彼得帕克原計劃在歐洲旅途中向自己喜歡的女孩MJ表達自己的心意,不料途中遇到了狡猾的神秘客,而且還被「整」得非常悽慘,受傷還不算啥,主要是託尼史塔克送給他的高科技眼鏡「伊迪絲」還被神秘客騙走了,好在最後蜘蛛俠利用絕招「彼得一激靈」戰勝了神秘客,並拿回了伊迪絲。
  • 帶你揭開骨質瓷的神秘面紗
    我們一起來揭開它的神秘面紗吧。什麼是骨質瓷?骨質瓷(Bone china): 簡稱骨瓷,亦稱骨灰瓷,生產始於英國1821年底,骨灰和瓷石多少的比例被「標準化」,當時,人們認為以長石為熔劑含骨灰46%的坯料生產出的瓷器是好的產品。
  • 月球探索大發現,中國功不可沒,地幔神秘面紗正被逐步揭開
    雖然說在1960年到1970年曾經有美國、俄羅斯對月球相繼進行探索,甚至有人登上過月球,但是對於月球的了解依舊是非常的片面,所以到現在為止,科學家對這顆星球都充滿了極大的興趣,想要進一步的揭開這顆星球的神秘面紗。現在隨著中國探測器登陸月球,月球的很多奧秘正在逐步的被揭開,尤其是地幔深層的物質。
  • 聯想新款平板電腦YOGA TABLET即將揭開神秘面紗
    文: 驅動中國10月29日消息,網上傳說很久的聯想平板電腦YOGA TABLET,即將揭開神秘面紗
  • 核輻射究竟是怎麼一回事?一起來揭開它的神秘面紗
    生活中輻射無處不在,它是物質能量傳播、釋放、轉換的一種方式。其中,核輻射是人類認識相對較晚的一種輻射形式,也是最為神秘的一種。前不久美劇排行榜上最熱門的《車諾比》,即通過藝術創作,演繹出核事故災難場景,再次引發不少人對核輻射的關注。核輻射究竟是怎麼一回事?
  • 揭開英特爾酷睿神秘面紗,i3 vs i5 vs i7到底該買哪一款?
    現在是揭開神秘面紗的時候了。這裡,不會涉及英特爾的其他處理器,如奔騰系列或新的以筆記本電腦為中心的Core M系列。Core i7,Core i5和Core i3之間的差異英特爾酷睿i7優於酷睿i5,而酷睿i5則優於酷睿i3。
  • 揭開三星QLED 8K電視的神秘面紗,它究竟有何魔力
    打開APP 揭開三星QLED 8K電視的神秘面紗,它究竟有何魔力 電子觀察說 發表於 2021-01-08 09:18:47 受新冠疫情的影響
  • 帶你揭開黑枸杞的神秘面紗
    但它真的有這麼神奇麼?今天就來為大家揭開它的神秘面紗。01黑枸杞到底是啥?黑枸杞,又稱「黑果枸杞」、「墨果枸杞」、「蘇枸杞」等,來自茄科枸杞屬多荊刺灌木。是目前市面上非常熱門的養身保健品之一。
  • 「千角燈系列」東莞非遺文創作品揭開神秘面紗
    11月28日上午,「千角燈系列」東莞非遺文創作品發布會在東莞市文化館非遺展廳舉行,揭開了千角燈系列非遺文創作品的神秘面紗。該系列作品由市內兩家設計企業(拼酷、好合苑)以國家級非遺項目千角燈為題材,前後醞釀和設計近半年時間完成,內含千角燈手鍊、千角燈茶壺、千角燈茶漏、千角燈伴手禮盒、千角燈3D金屬拼圖等,彰顯創意和實用性。
  • 《刺客信條》中文故事導覽 揭開聖殿騎士的神秘面紗
    《刺客信條》中文故事導覽 揭開聖殿騎士的神秘面紗 《刺客信條》中文故事導覽 揭開聖殿騎士的神秘面紗