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

背景

衆所周知, Java 或者其餘運行在 JVM(java 虛擬機)上面的程序都須要最終便覺得字節碼,而後被 JVM加載運行,那麼這個加載到虛擬機的過程就是 classloader 類加載器所幹的事情.直白一點,就是 經過一個類的全限定類名稱來獲取描述此類的二進制字節流 的過程.java

雙親委派模型

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

  1. 啓動類加載器(Bootstrap ClassLoader), 由 C++語言實現,是虛擬機自身的一部分.
  2. 其餘的類加載器,都是由 Java 實現,在虛擬機的外部,而且所有繼承自java.lang.ClassLoader

在 Java 內部,絕大部分的程序都會使用 Java 內部提供的默認加載器.web

啓動類加載器(Bootstrap ClassLoader)

負責將$JAVA_HOME/lib或者 -Xbootclasspath 參數指定路徑下面的文件(按照文件名識別,如 rt.jar) 加載到虛擬機內存中.啓動類加載器沒法直接被 java 代碼引用,若是須要把加載請求委派給啓動類加載器,直接返回null便可.api

擴展類加載器(Extension ClassLoader)

負責加載$JAVA_HOME/lib/ext 目錄中的文件,或者java.ext.dirs 系統變量所指定的路徑的類庫.tomcat

應用程序類加載器(Application ClassLoader)

通常是系統的默認加載器,好比用 main 方法啓動就是用此類加載器,也就是說若是沒有自定義過類加載器,同時它也是getSystemClassLoader() 的返回值.app

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

image.png

工做流程:ide

  1. 收到類加載的請求
  2. 首先不會本身嘗試加載此類,而是委託給父類的加載器去完成.
  3. 若是父類加載器沒有,繼續尋找父類加載器.
  4. 搜索了一圈,發現都找不到,而後纔是本身嘗試加載此類.

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

可是這種模型只是一種推薦的方式,並非強制的,你也能夠嘗試打破這種規則. 自因此這樣約定,仍是有必定的好處的, Java 類隨着它的類加載器一塊兒具有了一種帶有優先級的層次關係. 好比本身定義了java.lang.Object 對象,那麼按照上面的流程,他永遠都是被啓動類加載器加載的rt.jar 中的那個類,而不是本身定義的這個類,這樣就保證了兄運行的穩定,不然,可能變得很是混亂,能夠隨意改寫任何類.spa

在 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 虛擬機(第二版)
相關文章
相關標籤/搜索