深入理解Java ClassLoader及在 JavaAgent 中的應用
背景
眾所周知, 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
: -
被載入的類定義,然後我們將這個類放到不是原始碼的路徑比如我放到
/Users/lican/git/test/foo/
這裡的,主要是方便測試.
然後測試程式為:
我們用FooClassLoader
來載入com.example.test.FooTest
, 然後在 AppClassLoader中持有引用.被後續使用.
PS:關注360linker公眾號,加入官方社群獲取免費視訊教程、知名單位招聘資訊。交流分享IT圈學習經驗。