深入理解Java ClassLoader及在 JavaAgent 中的应用
轉載自? ?深入理解Java ClassLoader及在 JavaAgent 中的應用
背景
眾所周知, Java 或者其他運行在 JVM(java 虛擬機)上面的程序都需要最終便以為字節碼,然后被 JVM加載運行,那么這個加載到虛擬機的過程就是 classloader 類加載器所干的事情.直白一點,就是 通過一個類的全限定類名稱來獲取描述此類的二進制字節流 的過程.
雙親委派模型
說到 Java 的類加載器,必不可少的就是它的雙親委派模型,從 Java 虛擬機的角度來看,只存在兩種不同的類加載器:
在 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 的加載使用,
/Users/lican/git/test/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 虛擬機(第二版)
?
總結
以上是生活随笔為你收集整理的深入理解Java ClassLoader及在 JavaAgent 中的应用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 尔湾在美国哪个城市 尔湾是在美国哪个城市
- 下一篇: Java Agent的隔离实现以及卸载时