背景
眾所周知, Java 或者其他運(yùn)行在 JVM(java 虛擬機(jī))上面的程序都需要最終便以為字節(jié)碼,然后被 JVM加載運(yùn)行,那么這個(gè)加載
到虛擬機(jī)的過(guò)程就是 classloader 類(lèi)加載器所干的事情.直白一點(diǎn),就是 通過(guò)一個(gè)類(lèi)的全限定類(lèi)名稱(chēng)來(lái)獲取描述此類(lèi)的二進(jìn)制字節(jié)流 的過(guò)程.
雙親委派模型
說(shuō)到 Java 的類(lèi)加載器,必不可少的就是它的雙親委派模型,從 Java 虛擬機(jī)的角度來(lái)看,只存在兩種不同的類(lèi)加載器:
- 啟動(dòng)類(lèi)加載器(Bootstrap ClassLoader), 由 C++語(yǔ)言實(shí)現(xiàn),是虛擬機(jī)自身的一部分.
- 其他的類(lèi)加載器,都是由 Java 實(shí)現(xiàn),在虛擬機(jī)的外部,并且全部繼承自
java.lang.ClassLoader
在 Java 內(nèi)部,絕大部分的程序都會(huì)使用 Java 內(nèi)部提供的默認(rèn)加載器.
啟動(dòng)類(lèi)加載器(Bootstrap ClassLoader)
負(fù)責(zé)將$JAVA_HOME/lib
或者 -Xbootclasspath
參數(shù)指定路徑下面的文件(按照文件名識(shí)別,如 rt.jar) 加載到虛擬機(jī)內(nèi)存中.啟動(dòng)類(lèi)加載器無(wú)法直接被 java 代碼引用,如果需要把加載請(qǐng)求委派給啟動(dòng)類(lèi)加載器,直接返回null
即可.
擴(kuò)展類(lèi)加載器(Extension ClassLoader)
負(fù)責(zé)加載$JAVA_HOME/lib/ext
目錄中的文件,或者java.ext.dirs
系統(tǒng)變量所指定的路徑的類(lèi)庫(kù).
應(yīng)用程序類(lèi)加載器(Application ClassLoader)
一般是系統(tǒng)的默認(rèn)加載器,比如用 main 方法啟動(dòng)就是用此類(lèi)加載器,也就是說(shuō)如果沒(méi)有自定義過(guò)類(lèi)加載器,同時(shí)它也是getSystemClassLoader()
的返回值.
這幾種類(lèi)加載器的工作流程被抽象成一個(gè)模型,就是雙親委派模型.
工作流程:
- 收到類(lèi)加載的請(qǐng)求
- 首先不會(huì)自己嘗試加載此類(lèi),而是委托給父類(lèi)的加載器去完成.
- 如果父類(lèi)加載器沒(méi)有,繼續(xù)尋找父類(lèi)加載器.
- 搜索了一圈,發(fā)現(xiàn)都找不到,然后才是自己嘗試加載此類(lèi).
這基本就是雙親委派模型.
但是這種模型只是一種推薦的方式,并不是強(qiáng)制的,你也可以嘗試打破這種規(guī)則.
自所以這樣約定,還是有一定的好處的, Java 類(lèi)隨著它的類(lèi)加載器一起具備了一種帶有優(yōu)先級(jí)的層次關(guān)系.
比如自己定義了java.lang.Object
對(duì)象,那么按照上面的流程,他永遠(yuǎn)都是被啟動(dòng)類(lèi)加載器加載的rt.jar 中的那個(gè)類(lèi),而不是自己定義的這個(gè)類(lèi),這樣就保證了兄運(yùn)行的穩(wěn)定,否則,可能變得非常混亂,可以隨意改寫(xiě)任何類(lèi).
在 JavaAgent 中的應(yīng)用
大多數(shù)情況下,其實(shí)我們并不需要知道這些,因?yàn)槟愕某绦蛞矔?huì)運(yùn)行的非常正常,雖然像Tomcat
,Spring Boot
都有自己定義的類(lèi)加載器,但是我們?cè)诓挥藐P(guān)心的情況下也會(huì)運(yùn)行的好好地.
那么類(lèi)加載器可以被運(yùn)行在哪些地方呢?
- 從遠(yuǎn)程(或者文件)加載類(lèi),有時(shí)候需要加載的類(lèi)可能并不是在當(dāng)前的 classpath, 可能需要自己定義類(lèi)加載器去加載.
- 自己想實(shí)現(xiàn)一個(gè)
JavaAgent
來(lái)增強(qiáng)字節(jié)碼的時(shí)候.
JavaAgent 的使用后續(xù)文章補(bǔ)上.先上一張圖.
頂層是應(yīng)用代碼實(shí)際運(yùn)行的 ClassLoader, 可能是
Application ClassLoader
, 也有可能是 tomcat 的webapp ClassLoader
或者其他容器自定義的類(lèi)加載器,總是是真實(shí) 的用戶(hù)編寫(xiě)的代碼運(yùn)行的 classloader.我們?nèi)绻?code>javaagent中增強(qiáng)用戶(hù)或者用戶(hù)使用的包進(jìn)行增強(qiáng)的話(huà),必須實(shí)現(xiàn)一個(gè)自定義的 classloader 來(lái)"繼承"(委派)應(yīng)用代碼的類(lèi)加載器.為什么?
javaagent 的代碼永遠(yuǎn)都是被應(yīng)用類(lèi)加載器(
Application ClassLoader
)所加載,和應(yīng)用代碼的真實(shí)加載器無(wú)關(guān),舉個(gè)栗子,當(dāng)前運(yùn)行在 tomcat 中的代碼是webapp ClassLoader
加載的,如果啟動(dòng)參數(shù)加上-javaagent
, 這個(gè) javaagent 還是在Application ClassLoader
中加載的.按照上面的雙親委派模型,如果我們?cè)?javaagent 中想要訪(fǎng)問(wèn)應(yīng)用里面的 api 包或者類(lèi),這是不可能的,因?yàn)榘凑针p親委派模型,通俗來(lái)說(shuō)就是,子加載器可以訪(fǎng)問(wèn)父加載器中的類(lèi),但是反過(guò)來(lái)就行不通.
那么這個(gè)時(shí)候有沒(méi)有辦法能夠做到呢?
我們可以自定義自己的類(lèi)加載器繼承應(yīng)用代碼類(lèi)加載器(可以在 javaagent 中完成, javaagent 每加載一個(gè)類(lèi),就會(huì)回調(diào)傳回真實(shí)的類(lèi)加載器),然后我們?cè)?code>Application ClassLoader 中用自定義的類(lèi)加載器去加載子類(lèi),并創(chuàng)建好實(shí)例(
newInstance()
), 將實(shí)例的引用保存 在變量中.真實(shí)運(yùn)行的時(shí)候,就會(huì)通過(guò)這個(gè)變量,去訪(fǎng)問(wèn)我們自定義加載器的內(nèi)容,又由于我們的自定義類(lèi)加載器是繼承自應(yīng)用代碼的類(lèi)加載器的,所以自定義類(lèi)加載器中的代碼可以訪(fǎng)問(wèn)應(yīng)用的代碼.
總結(jié)一句就是,父類(lèi)加載器無(wú)法加載子類(lèi)加載器的類(lèi),但是可以持有子類(lèi)加載器所加載類(lèi)的實(shí)例,從而實(shí)現(xiàn)父類(lèi)加載器的代碼可以調(diào)用子類(lèi)加載器的代碼的形式
貌似比較抽象,后面會(huì)補(bǔ)上詳細(xì)的例子供參考.
例子
針對(duì)上面的情形,我們定義一個(gè)例子,可以詳細(xì)解釋 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;
}
}
- 被加載的類(lèi)定義,然后我們將這個(gè)類(lèi)放到不是源代碼的路徑比如我放到
/Users/lican/git/test/foo/
這里的,主要是方便測(cè)試.
package com.example.test;
public class FooTest {
public String getFoo() {
return "foo";
}
}
然后測(cè)試程序?yàn)?
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
來(lái)加載com.example.test.FooTest
, 然后在 AppClassLoader中持有引用.被后續(xù)使用.
引用
- 深入理解 Java 虛擬機(jī)(第二版)