深入理解Java ClassLoader及在 JavaAgent 中的應用

candyleer發表於2019-02-23

背景

眾所周知, Java 或者其他執行在 JVM(java 虛擬機器)上面的程式都需要最終便以為位元組碼,然後被 JVM載入執行,那麼這個載入到虛擬機器的過程就是 classloader 類載入器所幹的事情.直白一點,就是 通過一個類的全限定類名稱來獲取描述此類的二進位制位元組流 的過程.

雙親委派模型

說到 Java 的類載入器,必不可少的就是它的雙親委派模型,從 Java 虛擬機器的角度來看,只存在兩種不同的類載入器:

  1. 啟動類載入器(Bootstrap ClassLoader), 由 C++語言實現,是虛擬機器自身的一部分.
  2. 其他的類載入器,都是由 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() 的返回值.

這幾種類載入器的工作流程被抽象成一個模型,就是雙親委派模型.

image.png

工作流程:

  1. 收到類載入的請求
  2. 首先不會自己嘗試載入此類,而是委託給父類的載入器去完成.
  3. 如果父類載入器沒有,繼續尋找父類載入器.
  4. 搜尋了一圈,發現都找不到,然後才是自己嘗試載入此類.

這基本就是雙親委派模型.

但是這種模型只是一種推薦的方式,並不是強制的,你也可以嘗試打破這種規則. 自所以這樣約定,還是有一定的好處的, Java 類隨著它的類載入器一起具備了一種帶有優先順序的層次關係. 比如自己定義了java.lang.Object 物件,那麼按照上面的流程,他永遠都是被啟動類載入器載入的rt.jar 中的那個類,而不是自己定義的這個類,這樣就保證了兄執行的穩定,否則,可能變得非常混亂,可以隨意改寫任何類.

在 JavaAgent 中的應用

大多數情況下,其實我們並不需要知道這些,因為你的程式也會執行的非常正常,雖然像Tomcat,Spring Boot 都有自己定義的類載入器,但是我們在不用關心的情況下也會執行的好好地.

那麼類載入器可以被執行在哪些地方呢?

  • 從遠端(或者檔案)載入類,有時候需要載入的類可能並不是在當前的 classpath, 可能需要自己定義類載入器去載入.
  • 自己想實現一個JavaAgent來增強位元組碼的時候.

JavaAgent 的使用後續文章補上.先上一張圖.

image.png

  • 頂層是應用程式碼實際執行的 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 的載入使用,

  1. 假如我們有如下的 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;
    }

}
複製程式碼
  1. 被載入的類定義,然後我們將這個類放到不是原始碼的路徑比如我放到 /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 虛擬機器(第二版)

相關文章