經過源碼,實例詳解java類加載機制

以前的文章中,介紹了class的字節碼靜態結構,這些類須要jvm加載到其在內存中分配的運行時數據區纔會生效,這個過程包含:加載 -> 連接 -> 初始化 幾個階段,其中連接階段又有驗證 -> 準備 -> 解析三個部分,接下來我會用三篇文章分別詳細介紹這三個階段,本文先介紹jvm類的加載以及雙親委派模型的概念。java

注意:類加載包含了從字節碼流到jvm方法區java.lang.Class對象建立並初始化整個過程,而本文介紹的類的加載只是其中一個階段shell

運行時數據區

本文內容基於hotspot jvm,運行時Class對象就存儲在Method Area中設計模式

類加載時機

何時開始加載一個類的字節碼呢?對此jvm規範並無給出明肯定義,可是jvm規範明確規定了類初始化的時機,根據類的加載發生在其初始化以前,能夠反推出類其加載的觸發條件。api

注:本文的類是泛指,還包括接口等數組

jvm規範定義了有且僅有5種狀況下若是類尚未初始化,會觸發類的初始化:緩存

  • 虛擬機啓動所指定的主類(包含main方法的類)先被加載並初始化
  • 初始化一個字類的時候,遞歸初始化其父類
  • 執行new, getstatic, putstatic, invokestatic指令時
  • 經過反射調用使用類時
  • java.lang.invoke.MethodHandle實例解析結果爲REF_getStatic, REF_putStatic, REF_invokeStatic方法句柄時

上面幾種狀況都很好理解,當前類引用了某個類而且使用了它,天然須要初始化,也天然要加載它,能夠經過-XX:+TraceClassLoading查看加載的類。關於初始化,之後的文章會單獨詳細介紹tomcat

類加載器

類的加載就是把一個類的字節碼靜態結構經過jvm加載,並建立一個對應的java.lang.Class對象,存儲在本身的運行時方法區內存空間,此後,這個類的數據便經過這個Class對象來訪問,包括其類field,方法等。網絡

字節碼不只是侷限於本地文件系統中的文件,也多是在內存中(動態生成),網絡上,壓縮包(jar, war)等,而類加載器的職責就是從這些地方加載字節碼到jvm中。架構

類加載器按其實現能夠分爲兩類:引導類加載器(Bootstrap Class Loader),用戶類加載器(User-defined Class Loader)app

  • 引導類加載器:加載$JAVA_HOME/jre/lib/下核心類庫,如rt.jar,hotspot jvm中由C++實現

  • 用戶類加載器:全部用戶類加載器都繼承了java.lang.ClassLoader抽象類,sun提供了兩個用戶類加載器,咱們也能夠定義本身的類加載器

    • 擴展類加載器(ExtClassLoader):sun.misc.Launcher$ExtClassLoader,負責加載$JAVA_HOME/jre/lib/ext下的一些擴展類

    • 應用類加載器(AppClassLoader):可由ClassLoader.getSystemClassLoader()方法得到,也稱系統類加載器,負責加載用戶(classpath中)定義的類。

    • 自定義類加載器(Custom ClassLoader):用戶也能夠定義本身的類加載器,實現一些定製的功能

關於類加載器補充幾點:

  1. 用戶類加載器都實現了 java.lang.ClassLoader抽象類,該類又個private final ClassLoader parent字段表示一個加載器的父加載器(設計模式中推薦使用這種組合的方式來代替繼承),這是實現雙親委派模型的關鍵。
  2. 引導類加載器之加載jre lib目錄(或-Xbootclasspath指定)下的類,而且只識別特定文件名如rt.jar,因此不會加載用戶的類
  3. 對於數組,並不存在數組類型的字節碼錶示形式,它由jvm負責建立,通常在碰到newarray指令進行初始化時,若是數組的元素類型不是基本類型(如int[]),而是引用類型(如Integer[]),則會先加載基本類型,這可能由引導類加載器或用戶類加載器加載,具體看引用類型是什麼。
  4. jvm會緩存已加載過的類,並設置加載相應類的加載器,見下文
  5. 一個類和加載它的類加載器(定義類加載器)共同肯定一個類的惟一性

下面經過實例來看一下:

/** * 自定義類加載器,重寫loadClass,優先在當前目錄加載 */
public class MyClassLoader extends ClassLoader {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        try {
            InputStream is = new FileInputStream("./" + name + ".class");
            byte[] data = new byte[is.available()];
            is.read(data);
            return defineClass(name, data, 0, data.length);
        } catch (IOException e) {
            return super.loadClass(name);
        }
    }
}
複製代碼
public class Callee {
    public Callee() {
        System.out.println("Callee class loaded by " + this.getClass().getClassLoader().getClass().getName());
    }
}
複製代碼
/** * Run: javac MyClassLoader.java Callee.java Test.java && java Test * / public class Test { public static void main(String[] args) throws Exception { ClassLoader myClassLoader = new MyClassLoader(); Class<?> calleeClass = myClassLoader.loadClass("Callee"); //輸出:calleeClass == Callee.class ? false System.out.println("calleeClass == Callee.class ? " + (calleeClass == Callee.class)); //輸出:Callee class loaded by sun.misc.Launcher$AppClassLoader Callee.class.newInstance(); //輸出:Callee class loaded by MyClassLoader Object calleeObj = calleeClass.newInstance(); } } 複製代碼

能夠看出,雖然是同一個類Callee,但因爲是不一樣類加載器加載,因此Class實例並非同一個。

雙親委派模型

所謂雙親委派模型是指一個類加載器在加載某個類時,首先把委派給父加載器去加載,父加載器又委派給它的父加載器加載,如此頂層的引導類加載器爲止,若是其父加載器在其搜索範圍沒有找到相應類,則嘗試本身加載。

從雙親委派模型的定義能夠看出,它要求每一個加載器都有一個父加載器,若是某個類加載器的父加載器爲null,則搜索引導類加載器是否加載過它要加載的類。

能夠看出首先接收加載請求的類加載器並不必定真正加載類,可能由它的父加載器完成加載,接收加載請求的類加載器叫作初始類加載器(initiating loader),而完成加載的類加載器叫作定義類加載器(defining loader),初始類加載器和定義類加載器可能相同也可能不一樣。

若是兩個類:D引用了C,L1做爲D的定義類加載器,在解析D時會去加載C,這個加載請求由L1接收,假設C由另外一個加載器L2加載,則L1最終將加載請求委託給L2,L1就稱爲C的初始加載器,L2是C的定義類加載器。

下面看看ClassLoader怎麼實現雙親委派加載的:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
    	 // 一個類的加載是放在代碼同步塊裏邊的,因此不會有同一個類加載屢次
        synchronized (getClassLoadingLock(name)) {
            // 首先檢查該類是否已加載過
            Class<?> c = findLoadedClass(name);
            // 若是緩存中沒有找到,則按雙親委派模型加載
            if (c == null) {
                try {
                    if (parent != null) {
                    	// 若是父加載器不爲null,則代理給父加載器加載
                    	// 父加載器在本身搜索範圍內找不到該類,則拋出ClassNotFoundException
                        c = parent.loadClass(name, false);
                    } else {
                    	// 若是父加載器爲null,則從引導類加載器加載過的類中
                    	// 找是否加載過此類,找不到返回null
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 存在父加載器但父加載器沒有找到要加載的類觸發此異常
                    // 只捕獲不處理,交給字加載器自身去加載
                }

                if (c == null) {
                    // 若是從父加載器到頂層加載器(引導類加載器)都找不到此類,則本身來加載
                    c = findClass(name);
                }
            }
            
            // 若是resolve指定爲true,則當即進入連接階段
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
複製代碼

經過源碼能夠看出,全部的類都優先委派給父加載器加載,若是父加載器沒法加載,則本身來加載,邏輯很簡單,這樣作的好處是不用層次的類交給不一樣的加載器去加載,如java.lang.Integer最終都是由Bootstrap ClassLoader來加載的,這樣只會有一個相同類被加載。

再來講說裏邊調用的幾個方法:

  • getClassLoadingLock
protected Object getClassLoadingLock(String className) {
    Object lock = this;
    if (parallelLockMap != null) {
        Object newLock = new Object();
        lock = parallelLockMap.putIfAbsent(className, newLock);
        if (lock == null) {
            lock = newLock;
        }
    }
    return lock;
}
複製代碼

該方法很簡單,parallelLockMap是一個ConcurrentHashMap<String, Object> map對象,若是當前classloader註冊爲可並行加載的,則爲每個類名維護一個鎖對象供synchronized使用,可並行加載不一樣類,不然以當前classloader做爲鎖對象,只能串行加載。

  • findBootstrapClassOrNull
private Class<?> findBootstrapClassOrNull(String name)
{
   if (!checkName(name)) 
   		return null;
   
   return findBootstrapClass(name);
}
複製代碼
private native Class<?> findBootstrapClass(String name);
複製代碼

findBootstrapClass是jvm原生實現,查找Bootstrap ClassLoader已加載的類,沒有則返回null

  • findClss
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}
複製代碼

findClass交給子加載器實現,咱們通常重寫該方法來實現本身的類加載器,這樣實現的類加載器也符合雙親委派模型。固然,雙親委派的邏輯都是在loadClass實現的,能夠本身重寫loadClass來打破雙親委派邏輯。

自定義類加載器:

/** * Run: javac MyClassLoader.java Callee.java Test.java && java Test */ public class MyClassLoader extends ClassLoader { @Override public Class<?> findClass(String name) throws ClassNotFoundException { try { InputStream is = new FileInputStream("./" + name + ".class"); byte[] data = new byte[is.available()]; is.read(data); return defineClass(name, data, 0, data.length); } catch (IOException e) { return super.loadClass(name); } } } public static void main(String[] args) throws Exception { ClassLoader myClassLoader = new MyClassLoader(); Class<?> callerClass = myClassLoader.loadClass("Callee"); // 輸出:Callee class loaded by sun.misc.Launcher$AppClassLoader callerClass.newInstance(); } 複製代碼

能夠看出,只需吧前面的示例方法名改成findClass就能夠了,並且能夠看到是由應用類加載器負責加載的(默認父加載器是AppClassLoader),符合雙親委派模型。

再來作個實驗:

// 讓自定義類加載器加載/tmp目錄下的類
InputStream is = new FileInputStream("/tmp/" + name + ".class");
複製代碼

把剛編譯的Callee.class移動至/tmp下(注意:當前目錄不要也保留一份):

mv Callee.class /tmp
複製代碼

再次編譯運行:

javac MyClassLoader.java && java Test
複製代碼

結果:

Callee class loaded by MyClassLoader
複製代碼

Callee變成由自定義類加載器加載了,由於向上委託時都找不到該類,自定義加載器findClass方法起了做用。

再來作個有趣的實驗:

定義一個類Caller裏邊調用了Callee

public class Caller {
    public Caller() {
        System.out.println("Caller class loaded by " + this.getClass().getClassLoader().getClass().getName());
        Callee callee = new Callee();
    }
}
複製代碼

修改Test.java,加載Caller

Class<?> callerClass = myClassLoader.loadClass("Caller");
複製代碼

再次編譯運行:

javac MyClassLoader.java Caller.java Test.java
mv Callee.class /tmp # 保證當前目錄下沒有Callee.class,/tmp下有
java Test
複製代碼

爲何/tmp下有Callee.class但沒有加載到呢?其實很好理解:輸出第一句看出AppClassLoader加載了Caller.class,做爲它的定義類加載器,當Caller中使用了Callee須要加載Callee.class的時候,AppClassLoader就會做爲Callee.class的初始加載器去加載它,根據雙親委派模型,最後AppClassLoader調用本身的findClass嘗試本身加載,classpath下沒有這個類,確定找不到~

這個例子還能夠看出:真正去加載類的類加載器(調用findClass方法)找不到類拋出ClassNotFoundException,此異常被封裝成NoClassDefFoundError拋出給使用的地方(初始類加載器),這種錯誤很常見。

反雙親委派模型

雙親委派模型很好的解決了加載類統一的問題,類加載都是由子加載器向上委派給父加載器加載,這樣加載的類具備層次,但若是在父加載器加載的類中又要調用子加載器加載的類怎麼辦呢?

好比兩個加載器L1,L2(L1 extends L2),L2加載了類A,類A中使用了類B(類B在L1搜索範圍內,應由L1加載),則L2做爲類B的初始加載器並向上委託父加載器加載,最終,父加載器加載失敗,L2嘗試本身加載。能夠想象,L2在本身搜索範圍也找不到類B,最終加載失敗。

要解決這個問題就要適當打破雙親委派模型的限制:

  • Thread Context Class Loader

線程上下文加載器, 最典型的應用場景就是SPI技術,像JDBC,JNDI,JAXP等,接口規範都是由java核心類庫來定義的,而規範的具體實現則是由不一樣廠商提供的,要在類庫代碼中調用用戶代碼時,就需經過線程上下文加載器來完成了。

能夠經過Thread對象的setContextClassLoader方法設置當前線程上下文加載器,若是沒有設置,則從父線程繼承,若是父線程也沒有設置過,那麼就取應用類加載器(AppClassLoader)做爲線程上下文加載器。

//ServiceLoader.java
public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

private ServiceLoader(Class<S> svc, ClassLoader cl) {
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}
複製代碼
  • tomcat類加載機制

tomcat採用不一樣的類加載機制,主要爲了解決兩個問題:

  1. 公共的類庫(如servlet-api.jar)須要共享
  2. 不一樣的應用能夠依賴同一類的不一樣版本,不一樣應用相互隔離,互不影響

瞭解了目的,再來看看tomcat採起了那種措施:

本文不打算介紹源碼,之後我會寫一個tomcat源碼系列

注:不一樣的jvm實現不太相同,這裏的Bootstrap泛指hotspot中的Bootstrap和ExtClassloader

這是tomcat6以前的架構,common,Server(Catalina),Shared分別加載tomcat /common,/server, /shared 下的類,不過如今的版本(tomcat9)中若是配置了server.loader,shared.loader依然適用。

若是沒有配置,tomcat依然建立commonLoader,catalinaLoader,sharedLoader三個類加載器(都是common類加載器實例,加載/lib目錄下的類),因此通常架構以下:

如今再來看:

  1. common加載器遵循雙親加載模型,基本類庫不重複
  2. WebappX加載器對應每一個應用一個,加載/WEB-INF/classes, /WEB-INF/lib/*下的類,應用級別隔離

問題完美解決,WebappX加載順序:

  1. 先交給Bootstrap loader加載
  2. 在應用/WEB-INF/classes下查找加載
  3. 在應用/WEB-INF/lib/*.jar下查找加載
  4. 轉交給系統類加載器加載(classpath)
  5. 交給Common 類加載器加載(/lib)

能夠看到/class, /lib目錄下類加載優先級高於系統類加載器和common類加載器。

能夠配置<Loader delegate="true"/>強行讓其按雙親委派模型加載

原文地址:原文

往期內容:

詳解字節碼(class)文件

讀取class文件

歡迎關注!

相關文章
相關標籤/搜索