
-
「MoreThanJava」 宣揚的是 「學習,不止 CODE」。 -
若是以爲 「不錯」 的朋友,歡迎 「關注 + 留言 + 分享」,文末有完整的獲取連接,您的支持是我前進的最大的動力!
前言
ClassLoader 能夠說是 Java 最爲神祕的功能之一了,好像你們都知道怎麼回事兒 (雙親委派模型好像都都能說得出來...),又都說不清楚具體是怎麼一回事 (爲何須要須要有什麼實際用途就很模糊了...)。html

今天,咱們就來深度扒一扒,揭開它神祕的面紗!java

Part 1. 類加載是作什麼的?
首先,咱們知道,Java 爲了實現 「一次編譯,處處運行」 的目標,採用了一種特別的方案:先 編譯 爲 與任何具體及其環境及操做系統環境無關的中間代碼(也就是 .class
字節碼文件),而後交由各個平臺特定的 Java 解釋器(也就是 JVM)來負責 解釋 運行。git

ClassLoader (顧名思義就是類加載器) 就是那個把字節碼交給 JVM 的搬運工 (加載進內存)。它負責將 字節碼形式 的 Class 轉換成 JVM 中 內存形式 的 Class 對象。程序員

字節碼能夠是來自於磁盤上的 .class
文件,也能夠是 jar
包裏的 *.class
,甚至是來自遠程服務器提供的字節流。字節碼的本質其實就是一個有特定複雜格式的字節數組 byte[]
。 (從後面解析 ClassLoader 類中的方法時更能體會)github

另外,類加載器不光能夠把 Class 加載到 JVM 之中並解析成 JVM 統一要求的對象格式,還有一個重要的做用就是 審查每一個類應該由誰加載。web
並且,這些 Java 類不會一次所有加載到內存,而是在應用程序須要時加載,這也是須要類加載器的地方。面試
Part 2. ClassLoader 類結構分析
如下就是 ClassLoader 的主要方法了:數據庫

-
defineClass()
用於將byte
字節流解析成 JVM 可以識別的 Class 對象。有了這個方法意味着咱們不只能夠經過.class
文件實例化對象,還能夠經過其餘方式實例化對象,例如經過網絡接收到一個類的字節碼。segmentfault(注意,若是直接調用這個方法生成類的 Class 對象,這個類的 Class 對象尚未
resolve
,JVM 會在這個對象真正實例化時才調用resolveClass()
進行連接)數組 -
findClass()
一般和defineClass()
一塊兒使用,咱們須要直接覆蓋 ClassLoader 父類的findClass()
方法來實現類的加載規則,從而取得要加載類的字節碼。(如下是 ClassLoader 源碼)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.String
protected 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
方法須要被定義爲public static void
的緣由了——咱們須要在類沒有加載時訪問),開始執行。
在真正的運行 main
方法以前,JVM 須要 加載、連接 以及 初始化 上述的 Xxxx 類。
第一步:加載(Loading)
這一步是讀取到類文件產生的二進制流(findClass()
),並轉換爲特定的數據結構(defineClass()
),初步校驗 cafe babe
魔法數 (二進制中前四個字節爲 0xCAFEBABE
用來標識該文件是 Java 文件,這是不少軟件的作法,好比 zip壓縮文件
)、常量池、文件長度、是否有父類等,而後在 Java 堆 中建立對應類的 java.lang.Class
實例,類中存儲的各部分信息也須要對應放入 運行時數據區 中(例如靜態變量、類信息等放入方法區)。
「如下是一個 Class 文件具備的基本結構的簡單圖示:
![]()
若是對 Class 文件更多細節感興趣的能夠進一步閱讀:https://juejin.im/post/6844904199617003528
這裏咱們可能會有一個疑問,爲何 JVM 容許尚未進行驗證、準備和解析的類信息放入方法區呢?
答案是加載階段和連接階段的部分動做(好比一部分字節碼文件格式驗證動做)是 交叉進行 的,也就是說 加載階段還沒完成,連接階段可能已經開始了。但這些夾雜在加載階段的動做(驗證文件格式等)仍然屬於連接操做。
第二步:連接(Linking)
Link 階段包括驗證、準備、解析三個步驟。下面👇咱們來詳細說說。
驗證:確保被加載的類的正確性
驗證是鏈接階段的第一步,這一階段的目的是 爲了確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。驗證階段大體會完成 4
個階段的檢驗動做:
-
文件格式驗證: 驗證字節流是否符合 Class 文件格式的規範;例如:是否以 0xCAFEBABE
開頭、主次版本號是否在當前虛擬機的處理範圍以內、常量池中的常量是否有不被支持的類型。 -
元數據驗證: 對字節碼描述的信息進行語義分析(注意:對比 javac
編譯階段的語義分析),以保證其描述的信息符合 Java 語言規範的要求;例如:這個類是否有父類,除了java.lang.Object
以外。 -
字節碼驗證: 經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的。 -
符號引用驗證: 確保解析動做能正確執行。
驗證階段是很是重要的,但不是必須的,它對程序運行期沒有影響,若是所引用的類通過反覆驗證,那麼能夠考慮採用 -Xverifynone
參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。
準備:爲類的靜態變量分配內存,並將其初始化爲默認值
準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些內存都將在 方法區 中分配。對於該階段有如下幾點須要注意:
-
1️⃣ 這時候進行內存分配的 僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨着對象一塊分配在 Java 堆中。
-
2️⃣ 這裏所設置的 初始值一般狀況下是數據類型默認的零值(如
0
、0L
、null
、false
等),而不是被在 Java 代碼中被顯式地賦予的值。 -
3️⃣ 若是類字段的字段屬性表中存在 ConstantValue 屬性,即 同時被
final
和static
修飾,那麼在準備階段變量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
查看類的詳細信息 (爲了節省篇幅只截取了 main
方法反編譯後的代碼):
// 上面是類的詳細信息省略...
{
// .....
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
,並無將字符串存入局部變量表中,這裏的字符串就是一個 直接引用。
第三步:初始化(Initialization)
初始化,爲類的靜態變量賦予正確的初始值,JVM 負責對類進行初始化,主要對類變量進行初始化。在 Java 中對類變量進行初始值設定有兩種方式:
-
1️⃣ 聲明類變量是指定初始值; -
2️⃣ 使用靜態代碼塊爲類變量指定初始值;
JVM 初始化步驟:
-
1️⃣ 假如這個類尚未被加載和鏈接,則程序先加載並鏈接該類 -
2️⃣ 假如該類的直接父類尚未被初始化,則先初始化其直接父類 -
3️⃣ 假如類中有初始化語句,則系統依次執行這些初始化語句
類初始化時機:只有當對類的主動使用的時候纔會致使類的初始化,類的主動使用包括如下幾種:
-
建立類的實例,也就是 new
的方式 -
訪問某個類或接口的靜態變量,或者對該靜態變量賦值 -
調用類的靜態方法 -
反射(如 Class.forName("com.wmyskxz.Tester")
) -
初始化某個類的子類,則其父類也會被初始化 -
Java 虛擬機啓動時被標明爲啓動類的類,直接使用 java.exe
命令來運行某個主類 -
使用 JDK 7 新加入的動態語言支持時,若是一個 java.lang.invoke.MethodHanlde
實例最後的解析結果爲REF_getstatic
、REF_putstatic
、REF_invokeStatic
、REF_newInvokeSpecial
四種類型的方法句柄時,都須要先初始化該句柄對應的類 -
接口中定義了 JDK 8 新加入的默認方法( default
修飾符), 實現類在初始化以前須要先初始化其接口
Part 4. 深刻理解雙親委派模型
咱們在上面👆已經瞭解了一個類是如何被加載進 JVM 的——依靠類加載器——在 Java 語言中自帶有三個類加載器:
-
Bootstrap ClassLoader 最頂層的加載類,主要加載 核心類庫, %JRE_HOME%\lib
下的rt.jar
、resources.jar
、charsets.jar
和class
等。 -
Extention ClassLoader 擴展的類加載器,加載目錄 %JRE_HOME%\lib\ext
目錄下的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@18b4aac2
Classloader of Logging:sun.misc.Launcher$ExtClassLoader@60e53b93
Classloader of ArrayList:null
如咱們所見,這裏分別對應三種不一樣類型的類加載器:AppClassLoader、ExtClassLoader 和 BootstrapClassLoader(顯示爲 null
)。
一個很好的問題是:Java 類是由 java.lang.ClassLoader
實例加載的,但類加載器自己也是類,那麼誰來加載類加載器呢?
咱們僞裝不知道,先來跟着源碼一步一步來看。
先來看看 Java 虛擬機入口代碼
在 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
包的路徑。
(ps: 能夠本身嘗試輸出一下 System.getProperty("sun.boot.class.path")
的內容,它正好對應了 JDK 目錄 lib
和 classes
目錄下的 jar
包——也就是一般你配置環境變量時設置的 %JAVA_HOME/lib
的目錄了——一樣的方式你也能夠看看 Ext 和 App 的源碼)
3️⃣ ExtClassLoader 和 AppClassLoader 都繼承自 URLClassLoader,進一步查看 ClassLoader 的繼承樹,傳說中的雙親委派模型也並無出現。(甚至看不到 Bootstrap ClassLoader 的影子,Ext 也沒有直接繼承自 App 類加載器)

(⚠️注意,這裏能夠明確看到每個 ClassLoader 都有一個 parent
變量,用於標識本身的父類,下面👇詳細說)
4️⃣ 注意如下代碼:
ClassLoader extcl;
extcl = ExtClassLoader.getExtClassLoader();
loader = AppClassLoader.getAppClassLoader(extcl);
分別跟蹤查看到這兩個 ClassLoader 初始化時的代碼:
// 一直追蹤到最頂層的 ClassLoader 定義,構造器的第二個參數標識了類加載器的父類
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
// 代碼省略.....
}
// Ext 設置本身的父類爲 null
public 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 體系內 (不是一個 Java 類,沒辦法直接表示爲 ExtClassLoader 的父加載器),因此輸出爲 null
。
(咱們能夠很輕易跟蹤到 findBootstrapClass()
方法被 native
修飾:private native Class<?> findBootstrapClass(String name);
)
➡️ 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 (別稱 SystemClassLoader)。
這也就是咱們熟知的最終的雙親委派模型了。

Part 5. 實現本身的類加載器
什麼狀況下須要自定義類加載器
在學習了類加載器的實現機制以後,咱們知道了雙親委派模型並不是強制模型,用戶能夠自定義類加載器,在什麼狀況下須要自定義類加載器呢?
1️⃣ 隔離加載類。在某些框架內進行中間件與應用的模塊隔離,把類加載器到不一樣的環境。好比,阿里內某容器框架經過自定義類加載器確保應用中依賴的 jar
包不會影響到中間件運行時使用的 jar
包。
2️⃣ 修改類加載方式。類的加載模型並不是強制,除了 Bootstrap 外,其餘的加載並不是必定要引入,或者根據實際狀況在某個時間點進行按需的動態加載。
3️⃣ 擴展加載源。好比從數據庫、網絡,甚至是電視機頂盒進行加載。(下面👇咱們會編寫一個從網絡加載類的例子)
4️⃣ 防止源碼泄露。Java 代碼容易被編譯和篡改,能夠進行編譯加密。那麼類加載器也須要自定義,還原加密的字節碼。
一個常規的例子
實現一個自定義的類加載器比較簡單:繼承 ClassLoader,重寫 findClass()
方法,調用 defineClass()
方法,就差很少行了。
Tester.java
咱們先來編寫一個測試用的類文件:
public class Tester {
public void say() {
System.out.println("關注【我沒有三顆心臟】,解鎖更多精彩!");
}
}
在同級目錄下執行 javac Tester.java
命令,並把編譯後的 Tester.class
放到指定的目錄下(我這邊爲了方便就放在桌面上啦 /Users/wmyskxz/Desktop
)
MyClassLoader.java
咱們編寫自定義 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 對象。
ClassLoaderTester 測試類
咱們須要刪除剛纔在項目目錄建立的 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";
}
}
(代碼來自:https://blog.csdn.net/justloveyou_/article/details/72217806)
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
「
本文已收錄至個人 Github 程序員成長系列 【More Than Java】,學習,不止 Code,歡迎 star:https://github.com/wmyskxz/MoreThanJava 我的公衆號 :wmyskxz, 我的獨立域名博客:wmyskxz.com,堅持原創輸出,下方掃碼關注,2020,與您共同成長!

(END)
很是感謝各位人才能 看到這裏,若是以爲本篇文章寫得不錯,以爲 「我沒有三顆心臟」有點東西 的話,求點贊,求關注,求分享,求留言!
創做不易,各位的支持和承認,就是我創做的最大動力,咱們下篇文章見!
本文分享自微信公衆號 - 我沒有三顆心臟(wmyskxz)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。