這篇關于JVM類加載器和雙親委派機制的筆記寫的太好了,建議收藏起來看

前言

Java里有如下幾種類加載器

  1. 啟動類加載器:負責加載支撐JVM運行的位于JRE的lib目錄下的核心類庫比如 rt.jar、charsets.jar等。
  2. 擴展類加載器(ExtClassLoader):負責加載支撐JVM運行的位于JRE的lib目錄下的ext擴展目錄中的JAR類包。
  3. 應用程序類加載器(AppClassLoader):負責加載ClassPath路徑下的類包,主要就是加載你自己寫的那些類。
  4. 自定義加載器:負責加載用戶自定義路徑下的類包。

通過以下實例來了解各個類加載器:

public class ClassLoaderTest {

    public static void main(String[] args) {
        System.out.println(Object.class.getClassLoader());
        // java提供的與DNS服務交互的api
        System.out.println(DNSNameService.class.getClassLoader());
        System.out.println(ClassLoaderTest.class.getClassLoader());

    }
}

運行結果如下:

null
sun.misc.Launcher$ExtClassLoader@6d6f6e28
sun.misc.Launcher$AppClassLoader@58644d46

啟動類加載器是在有jvm底層創建的實例,所以在獲取時為null,Object類是有啟動類加載器進行加載的,所以獲取其加載器時為null,而DNSNameService為JAVA_HOME/jre/lib目錄下ext文件夾在的dnsns.jar包中的類,由擴展類加載器(ExtClassLoader)加載。而自己編寫的類ClassLoaderTest 則由AppClassLoader進行加載。

Java中各個類加載器的層次關系

在上面已經介紹過,java的類加載器也是普通的類,ExtClassLoader和AppClassLoader均是URLClassLoader的子類,而URL的繼承關系如下:


那么AppClassLoader和ExtClassLoader為ClassLoader的子類。在上面已經已經介紹過在jvm啟動時會通過sun.misc.Launcher的getLauncher方法從而獲取Launcher的實例,那么在這個過程中Launcher會通過構造方法創建該類的實例。sun.misc.Launcher的構造方法如下:

public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
        // 創建ExtClassLoader
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }

    try {
        // 創建AppClassPoader
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }
      // 省略代碼 ....
    }
}

通過分析sun.misc.Launcher構造方法我們知道在sun.misc.Launcher類的實例創建是會創建AppClassLoader實例和ExtClassLoader實例。同時由于兩個類加載器均繼承自ClassLoader,而ClassLoader中有一個ClassLoader的全局變量parent,該類類型也是ClassLoader:

public abstract class ClassLoader {

    private static native void registerNatives();
    static {
        registerNatives();
    }

    // The parent class loader for delegation
    // Note: VM hardcoded the offset of this field, thus all new fields
    // must be added *after* it.
    private final ClassLoader parent;

    // 省略代碼 ....
}

而在sun.misc.Launcher創建時,實例化ExtClassLoader和AppClassLoader時均指定其parent屬性分別為null和ExtClassLoader。那么java中的類加載器的機構就如下:


自定義類加載器

自定義類加載器需要繼承 java.lang.ClassLoader 類,該類有兩個核心方法,一個是 loadClass(String, boolean),實現了雙親委派機制,大體邏輯

  1. 首先,檢查一下指定名稱的類是否已經加載過,如果加載過了,就不需要再 加載, 直接返回。

  2. 如果此類沒有加載過,那么,再判斷一下是否有父加載器;如果有父加載器, 則由父加載器加載(即調用parent.loadClass(name, false);).或者是調用 Bootstrap類加載器來加載。

  3. 如果父加載器及bootstrap類加載器都沒有找到指定的類,那么調用當前類 加載器 的findClass方法來完成類加載。還有一個方法是findClass,默認 實現是拋出異常,所以我們自定義類加載器主要是重寫 findClass方法。

接下來看一個示例,首先我們編寫需要自定義類加載器加載的類,如下:

package com.dp.jvm;

import java.io.PrintStream;

public class User
{
  public void say()
  {
    System.out.println("hello");
  }
}

需要注意的是,該類編寫完成需要在工程中刪除,避免AppClassLoader加載。編譯完成后將該類的class文件放置指定的目錄下:


然后編寫自定義的類加載器,代碼如下:

class MyClassLoader extends ClassLoader{

    private final String path;

    MyClassLoader(String path) {
        this.path = path;
    }

    /**
     * 重寫ClassLoader的findClass方法,獲取到類的Class對象
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] byteArrayFromClassName = getByteArrayFromClassName(name);
        return defineClass(name, byteArrayFromClassName, 0, byteArrayFromClassName.length);
    }

    /**
     * 通過類的全限定名稱獲取到類的二進制數據
     * @param name
     * @return
     */
    private byte[] getByteArrayFromClassName(String name) {
        String classPath = convertNameToPath(name);
        byte[] data = null;
        int off = 0;
        int length = 0;
        try(BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(classPath))) {
            data = new byte[bufferedInputStream.available()];
            while ((length = bufferedInputStream.read(data, off, data.length - off)) > 0) {
                off += length;
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return data;
    }

    /**
     * 通過類的全限定名稱獲取到對應類文件的的字節碼文件路徑
     * @param name
     * @return
     */
    private String convertNameToPath(String name) {
        String relativePath = name.replace(".", File.separator);
        String absolutePath = path + File.separator + relativePath + ".class";
        return absolutePath;
    }
}

編寫測試類,通過使用自定義的類加載將User加載并實例化,然后調用其say方法,如下:

public class CustomClassLoaderTest {

    public static void main(String[] args) throws Exception {
        MyClassLoader myClassLoader = new MyClassLoader("F:\\test");
        Class<?> clazz = myClassLoader.loadClass("com.dp.jvm.User");
        Object o = clazz.newInstance();
        Method say = clazz.getDeclaredMethod("say");
        say.invoke(o);
    }
}

通過上面了實例,簡單的實現了一個自定義的類加載器。接留下來了解一下類加載器的雙親委派機制。

雙親委派機制

JVM類加載器是有親子層級結構的,如下圖:


需要注意的是,這里的額親子層級結構不是指的java中的繼承關系,而是每一個類加載實現類都具有一個parent全局變量,而該全局變量的類型為ClassLoader。這里可能有一個疑問,在自定義類加載器中并未看到名稱parent的全局變量。這是因為這個全局變量是在ClassLoader中定義聲明的。

public abstract class ClassLoader {

    private static native void registerNatives();
    static {
        registerNatives();
    }

    // The parent class loader for delegation
    // Note: VM hardcoded the offset of this field, thus all new fields
    // must be added *after* it.
    private final ClassLoader parent;

    // 省略代碼 ....

還有一個問題也需要說明一下,我們自定義的類加載器的parent屬性是如何設置的呢?怎么知道設置的為AppClassLoader呢?因為自定義的類加載器繼承自ClassLoader,而ClassLoader中有一個無參的構造函數,如下:

protected ClassLoader() {
   //調用有參構造函數 
    this(checkCreateClassLoader(), getSystemClassLoader());
}

private ClassLoader(Void unused, ClassLoader parent) {
    //設置父加載器
    this.parent = parent;
    if (ParallelLoaders.isRegistered(this.getClass())) {
        parallelLockMap = new ConcurrentHashMap<>();
        package2certs = new ConcurrentHashMap<>();
        domains =
            Collections.synchronizedSet(new HashSet<ProtectionDomain>());
        assertionLock = new Object();
    } else {
        // no finer-grained lock; lock on the classloader instance
        parallelLockMap = null;
        package2certs = new Hashtable<>();
        domains = new HashSet<>();
        assertionLock = this;
    }
}

從ClassLoader的實現來看,通過getSystemClassLoader()方法獲取系統類加載器然后將其賦值給parent屬性。那么來看一下getSystemClassLoader()具體實現:

public static ClassLoader getSystemClassLoader() {
    initSystemClassLoader();
    if (scl == null) {
        return null;
    }
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkClassLoaderPermission(scl, Reflection.getCallerClass());
    }
    return scl;
}

// 初始化系統類加載器
private static synchronized void initSystemClassLoader() {
    if (!sclSet) {
        if (scl != null)
            throw new IllegalStateException("recursive invocation");
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
        if (l != null) {
            Throwable oops = null;
            scl = l.getClassLoader();
            try {
                scl = AccessController.doPrivileged(
                    new SystemClassLoaderAction(scl));
            } catch (PrivilegedActionException pae) {
                oops = pae.getCause();
                if (oops instanceof InvocationTargetException) {
                    oops = oops.getCause();
                }
            }
            if (oops != null) {
                if (oops instanceof Error) {
                    throw (Error) oops;
                } else {
                    // wrap the exception
                    throw new Error(oops);
                }
            }
        }
        sclSet = true;
    }
}

在getSystemClassLoader方法中獲取sun.misc.Lanucher實例(單例),然后調用其getClassLoader方法獲取系統類加載器,然后設置給parent方法。最后來看一下sun.misc.Lanucher的getClassLoader方法:

public ClassLoader getClassLoader() {
    return this.loader;
}

public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }

    try {
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }
}

結合sun.misc.Lanucher的getClassLoader和構造方法可知系統類加載器就是AppClassLoader。
在了解了jvm中類加載器的組成結構后,我們再來看一下jvm中各個類加載器的組成的結構:


在了解了jvm中各個類加載器的層次結構之后,加下來來解析雙親委派機制就相對來說簡單多了,首先從雙親委派的流程說起。

雙親委派流程

雙親委派流程如下:


加載某個類時會先委托父加載器尋找目標類,找不到再委托上層父加載器加載,如果所有父加載器在自己的加載類路徑下都找不到目標類,則在自己的類加載路徑中查找并載入目標類。
比如我們的PrintTest 類,最先會找應用程序類加載器加載,應用程序類加載器會先委托擴展類加載器加載,擴展類加載器再委托啟動類加載器,頂層啟動類加載器在自己的類加載路徑里找了半天沒找到PrintTest 類,則向下退回加載PrintTest 類的請求,擴展類加載器收到回復就自己加載,在自己的類加載路徑里找了半天也沒找到PrintTest 類,又向下退回PrintTest 類的加載請求給應用程序類加載器,應用程序類加載器于是在自己的類加載路徑里找Math類,結果找到了就自己加載了。
雙親委派機制說簡單點就是,先找父親加載,不行再由兒子自己加載。

那么為什么要設置雙親委派機制呢?

  • 沙箱安全機制:自己寫的java.lang.String.class類不會被加載,這樣便可以防止核心API庫被隨意篡改。
  • 避免類的重復加載:當父親已經加載了該類時,就沒有必要子ClassLoader再加載一次,保證被加載類的唯一性

雙親委派機制源碼剖析

雙親委派的原理體現在ClassLoader的loadClass方法中:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 首先檢查類是夠已經被加載
        Class<?> c = findLoadedClass(name);

        // 類還未加載則加載,使用雙親委派機制
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 判斷當前類加載器是否設置了父加載器,設置了則
                // 調用父加載器的loadClass進行加載,如果父加載也是ClassLoader
                // 的子類則會再次進入該方法,判斷是否有父類加載器,依次遞歸
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // 當類加載沒有設置parent父加載,那么就使用啟動類加載器加載
                    // 由于啟動類加載器是底層創建的實例,所以該方法會調用本地
                    // native方法
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {

            }

            if (c == null) {
                // 向上委派所有父加載器仍然沒有加載到參數類,那么調用當前
                // 類加載器進行類的加載
                long t1 = System.nanoTime();
                c = findClass(name);
                // ... 省略 

            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

打破雙親委派

以Tomcat類加載為例,Tomcat 如果使用默認的雙親委派類加載機制行不行?
我們思考一下:Tomcat是個web容器, 那么它要解決什么問題:

  1. 一個web容器可能需要部署兩個應用程序,不同的應用程序可能會依賴同一 個第三方類庫的不同版本,不能要求同一個類庫在同一個服務器只有一份, 因此要保證每個應用程序的類庫都是獨立的,保證相互隔離。比如:在tomcat容器中存在存在兩個應用A和B,A使用的是Spring4,而應用B使用的是Spring5,如果使用雙親委派,那么可能會導致版本沖突從而報錯,如果在版本4中不存在x方法,但是先加載了版本4的字節碼,那么版本5的就不會在加載了(類限定名相同),那么在程序B中調用x方法則會拋出方法不存在異常。
  2. 部署在同一個web容器中相同的類庫相同的版本可以共享。否則,如果服務器有10個應用程序,那么要有10份相同的類庫加載進虛擬機。我們常見的web應用中的Servlet依賴,一般在maven中依賴作用于都是provided的,web程序都是使用的容器的Serlvert,如果都是各自的那么造成類的重復加載。
  3. web容器也有自己依賴的類庫,不能與應用程序的類庫混淆。基于安全考慮,應該讓容器的類庫和程序的類庫隔離開來。
  4. web容器要支持jsp的修改,我們知道,jsp文件最終也是要編譯成class文件才能在虛擬機中運行,但程序運行后修改jsp已經是司空見慣的事情,web容器需要支持 jsp修改后不用重啟.了解jsp機制的都知道,是將jsp解析成一個對應的Servlet(就是常說的一個jsp就是一個servlet),jsp就是通過動態生成.class文件從而實現動態資源的。

再看看我們的問題:
Tomcat 如果使用默認的雙親委派類加載機制行不行?

  • 第一個問題,如果使用默認的類加載器機制,那么是無法加載兩個相同類庫的不同版本的,
    默認的類加器是不管你是什么版本的,只在乎你的全限定類名,并且只有一份。
  • 第二個問題,默認的類加載器是能夠實現的,因為他的職責就是保證唯一性。
  • 第三個問題和第一個問題一樣。
  • 我們再看第四個問題,我們想我們要怎么實現jsp文件的熱加載,jsp文件其實也就是class文件,那么如果修改了,但類名還是一樣,類加載器會直接取方法區中已經存在的,修改后的jsp是不會重新加載的。那么怎么辦呢?我們可以直接卸載掉這jsp文件的類加載器,所以你應該想到了,每個jsp文件對應一個唯一的類加載器,當一個jsp文件修改了,就直接卸載這個jsp類加載器。重新創建類加載器,重新加載jsp文件。

最后

感謝你看到這里,看完有什么的不懂的可以在評論區問我,覺得文章對你有幫助的話記得給我點個贊,每天都會分享java相關技術文章或行業資訊,歡迎大家關注和轉發文章!

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。