背景
眾所周知, Java 或者其他執行在 JVM(java 虛擬機器)上面的程式都需要最終便以為位元組碼,然後被 JVM載入執行,那麼這個載入
到虛擬機器的過程就是 classloader 類載入器所幹的事情.直白一點,就是 通過一個類的全限定類名稱來獲取描述此類的二進位制位元組流 的過程.
雙親委派模型
說到 Java 的類載入器,必不可少的就是它的雙親委派模型,從 Java 虛擬機器的角度來看,只存在兩種不同的類載入器:
- 啟動類載入器(Bootstrap ClassLoader), 由 C++語言實現,是虛擬機器自身的一部分.
- 其他的類載入器,都是由 Java 實現,在虛擬機器的外部,並且全部繼承自
java.lang.ClassLoader
在 Java 內部,絕大部分的程式都會使用 Java 內部提供的預設載入器.
啟動類載入器(Bootstrap ClassLoader)
負責將$JAVA_HOME/lib
或者 -Xbootclasspath
引數指定路徑下面的檔案(按照檔名識別,如 rt.jar) 載入到虛擬機器記憶體中.啟動類載入器無法直接被 java 程式碼引用,如果需要把載入請求委派給啟動類載入器,直接返回null
即可.
擴充套件類載入器(Extension ClassLoader)
負責載入$JAVA_HOME/lib/ext
目錄中的檔案,或者java.ext.dirs
系統變數所指定的路徑的類庫.
應用程式類載入器(Application ClassLoader)
一般是系統的預設載入器,比如用 main 方法啟動就是用此類載入器,也就是說如果沒有自定義過類載入器,同時它也是getSystemClassLoader()
的返回值.
這幾種類載入器的工作流程被抽象成一個模型,就是雙親委派模型.
工作流程:
- 收到類載入的請求
- 首先不會自己嘗試載入此類,而是委託給父類的載入器去完成.
- 如果父類載入器沒有,繼續尋找父類載入器.
- 搜尋了一圈,發現都找不到,然後才是自己嘗試載入此類.
這基本就是雙親委派模型.
但是這種模型只是一種推薦的方式,並不是強制的,你也可以嘗試打破這種規則.
自所以這樣約定,還是有一定的好處的, Java 類隨著它的類載入器一起具備了一種帶有優先順序的層次關係.
比如自己定義了java.lang.Object
物件,那麼按照上面的流程,他永遠都是被啟動類載入器載入的rt.jar 中的那個類,而不是自己定義的這個類,這樣就保證了兄執行的穩定,否則,可能變得非常混亂,可以隨意改寫任何類.
在 JavaAgent 中的應用
大多數情況下,其實我們並不需要知道這些,因為你的程式也會執行的非常正常,雖然像Tomcat
,Spring Boot
都有自己定義的類載入器,但是我們在不用關心的情況下也會執行的好好地.
那麼類載入器可以被執行在哪些地方呢?
- 從遠端(或者檔案)載入類,有時候需要載入的類可能並不是在當前的 classpath, 可能需要自己定義類載入器去載入.
- 自己想實現一個
JavaAgent
來增強位元組碼的時候.
JavaAgent 的使用後續文章補上.先上一張圖.
-
頂層是應用程式碼實際執行的 ClassLoader, 可能是
Application ClassLoader
, 也有可能是 tomcat 的webapp ClassLoader
或者其他容器自定義的類載入器,總是是真實 的使用者編寫的程式碼執行的 classloader. -
我們如果要在
javaagent
中增強使用者或者使用者使用的包進行增強的話,必須實現一個自定義的 classloader 來"繼承"(委派)應用程式碼的類載入器.為什麼? -
javaagent 的程式碼永遠都是被應用類載入器(
Application ClassLoader
)所載入,和應用程式碼的真實載入器無關,舉個栗子,當前執行在 tomcat 中的程式碼是webapp ClassLoader
載入的,如果啟動引數加上-javaagent
, 這個 javaagent 還是在Application ClassLoader
中載入的. -
按照上面的雙親委派模型,如果我們在 javaagent 中想要訪問應用裡面的 api 包或者類,這是不可能的,因為按照雙親委派模型,通俗來說就是,子載入器可以訪問父載入器中的類,但是反過來就行不通.
那麼這個時候有沒有辦法能夠做到呢?
-
我們可以自定義自己的類載入器繼承應用程式碼類載入器(可以在 javaagent 中完成, javaagent 每載入一個類,就會回撥傳回真實的類載入器),然後我們在
Application ClassLoader
中用自定義的類載入器去載入子類,並建立好例項(newInstance()
), 將例項的引用儲存 在變數中. -
真實執行的時候,就會通過這個變數,去訪問我們自定義載入器的內容,又由於我們的自定義類載入器是繼承自應用程式碼的類載入器的,所以自定義類載入器中的程式碼可以訪問應用的程式碼.
總結一句就是,父類載入器無法載入子類載入器的類,但是可以持有子類載入器所載入類的例項,從而實現父類載入器的程式碼可以呼叫子類載入器的程式碼的形式
貌似比較抽象,後面會補上詳細的例子供參考.
例子
針對上面的情形,我們定義一個例子,可以詳細解釋 ClassLoader 的載入使用,
- 假如我們有如下的 ClassLoader,
FooClassLoader
:
package com.example.test;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
/**
* @author lican
*/
public class FooClassLoader extends ClassLoader {
private static final String NAME = "/Users/lican/git/test/foo/";
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> loadedClass = findLoadedClass(name);
if (loadedClass == null) {
String s = name.substring(name.lastIndexOf(".") + 1) + ".class";
File file = new File(NAME + s);
try (FileInputStream fileInputStream = new FileInputStream(file)) {
byte[] b = new byte[fileInputStream.available()];
fileInputStream.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
e.printStackTrace();
}
}
return loadedClass;
}
}
複製程式碼
- 被載入的類定義,然後我們將這個類放到不是原始碼的路徑比如我放到
/Users/lican/git/test/foo/
這裡的,主要是方便測試.
package com.example.test;
public class FooTest {
public String getFoo() {
return "foo";
}
}
複製程式碼
然後測試程式為:
package com.example.test;
import java.lang.reflect.Method;
/**
* @author lican
*/
public class ClassLoaderTest {
private Object fooTestInstance;
private FooClassLoader fooClassLoader = new FooClassLoader();
public static void main(String[] args) throws Exception {
ClassLoaderTest classLoaderTest = new ClassLoaderTest();
classLoaderTest.initAndLoad();
Object fooTestInstance = classLoaderTest.getFooTestInstance();
System.out.println(fooTestInstance.getClass().getClassLoader());
Method getFoo = fooTestInstance.getClass().getMethod("getFoo");
System.out.println(getFoo.invoke(fooTestInstance));
System.out.println(classLoaderTest.getClass().getClassLoader());
}
private void initAndLoad() throws Exception {
Class<?> aClass = Class.forName("com.example.test.FooTest", true, fooClassLoader);
fooTestInstance = aClass.newInstance();
}
public Object getFooTestInstance() {
return fooTestInstance;
}
}
複製程式碼
我們用FooClassLoader
來載入com.example.test.FooTest
, 然後在 AppClassLoader中持有引用.被後續使用.
引用
- 深入理解 Java 虛擬機器(第二版)