Java 類(lèi)加載機(jī)制

何為類(lèi)加載

類(lèi)加載指的是JVM將class二進(jìn)制文件讀取到內(nèi)存方法區(qū),在堆內(nèi)存中生成Class對(duì)象。

類(lèi)加載過(guò)程

類(lèi)加載的過(guò)程包含如下步驟:

  • 加載
  • 驗(yàn)證
  • 準(zhǔn)備
  • 解析
  • 初始化

加載(Loading)

在這個(gè)階段,類(lèi)加載器(ClassLoader)負(fù)責(zé)查找和導(dǎo)入二進(jìn)制字節(jié)碼到 JVM 內(nèi)存中。它通過(guò)類(lèi)的全限定名(包名+類(lèi)名)找到對(duì)應(yīng)的 .class 文件或其他包含類(lèi)定義的數(shù)據(jù)源(比如 Jar 文件、網(wǎng)絡(luò)數(shù)據(jù)流等)。
加載完成后,在內(nèi)存中創(chuàng)建一個(gè) java.lang.Class 類(lèi)型的對(duì)象,該對(duì)象將作為方法區(qū)內(nèi)的類(lèi)元數(shù)據(jù)的入口,包含了與類(lèi)有關(guān)的各種信息。

驗(yàn)證(Verification)

驗(yàn)證階段是對(duì)加載的字節(jié)碼數(shù)據(jù)進(jìn)行合法性校驗(yàn),確保符合 JVM 規(guī)范。驗(yàn)證內(nèi)容包括文件格式驗(yàn)證、元數(shù)據(jù)驗(yàn)證、字節(jié)碼驗(yàn)證和符號(hào)引用驗(yàn)證等。
可以使用-Xverify:none或者-noverifyJVM參數(shù),禁用驗(yàn)證步驟,同時(shí)也會(huì)加快Java應(yīng)用的啟動(dòng)速度。(JDK13及其之后已廢棄此參數(shù))

準(zhǔn)備(Preparation)

準(zhǔn)備階段主要是為類(lèi)變量(static fields)分配內(nèi)存并設(shè)置初始值。這里的初始值通常是指數(shù)據(jù)類(lèi)型的零值(例如 int 類(lèi)型為 0,對(duì)象引用為 null),而不是程序員在 Java 源代碼中為它們賦予的初始值。對(duì)于 static final 常量,如果其值在編譯期就可以確定,則會(huì)在此階段直接賦值為常量池中的值。

解析(Resolution)

解析階段主要是將常量池中的符號(hào)引用替換為直接引用的過(guò)程。符號(hào)引用包括類(lèi)和接口的全限定名、字段名和方法名等,直接引用則更為具體,可以是內(nèi)存偏移量或句柄等。此階段主要針對(duì)類(lèi)或者接口、字段和方法的符號(hào)引用進(jìn)行解析。

初始化(Initialization)

初始化是類(lèi)加載過(guò)程的最后一步,真正執(zhí)行類(lèi)初始化語(yǔ)句(即類(lèi)構(gòu)造器 <clinit> 方法),為類(lèi)變量賦初始值或者執(zhí)行其他初始化邏輯。初始化只會(huì)執(zhí)行一次,且在類(lèi)首次主動(dòng)使用時(shí)觸發(fā),例如創(chuàng)建類(lèi)的實(shí)例、調(diào)用類(lèi)的靜態(tài)方法或訪問(wèn)類(lèi)的靜態(tài)字段時(shí)。

Java 類(lèi)加載器

Java 類(lèi)加載器是 Java 虛擬機(jī) (JVM) 中負(fù)責(zé)動(dòng)態(tài)加載 Java 類(lèi)到 JVM 運(yùn)行時(shí)數(shù)據(jù)區(qū)中的關(guān)鍵組件。在 Java 中,類(lèi)加載過(guò)程是 JVM 實(shí)現(xiàn)動(dòng)態(tài)性的重要手段,它通過(guò)不同的類(lèi)加載器協(xié)作完成對(duì)類(lèi)的查找、加載、鏈接(驗(yàn)證、準(zhǔn)備、解析)和初始化。

Java 類(lèi)加載器可分為以下幾種類(lèi)型:

  • Bootstrap ClassLoader 是最頂層的類(lèi)加載器,C語(yǔ)言實(shí)現(xiàn),是 JVM 的一部分,不繼承自 java.lang.ClassLoader 類(lèi)。它負(fù)責(zé)加載 Java 核心庫(kù),即 JDK 的 lib 目錄下的 rt.jar、resources.jar 等核心類(lèi)庫(kù),或被-Xbootclasspath參數(shù)指定路徑中的class文件。如java.*開(kāi)頭的類(lèi)。無(wú)法使用loader.getParent()方法獲取。
  • ExtClassLoader 由 Java 編寫(xiě),繼承自 ClassLoader 類(lèi)。負(fù)責(zé)加載$JAVA_HOME/lib/ext目錄中的class,以及被java.ext.dirs系統(tǒng)變量指定路徑中的class。如javax.*開(kāi)頭的類(lèi)。
  • AppClassLoader 負(fù)責(zé)加載用戶類(lèi)路徑(classpath,java.class.path系統(tǒng)變量)中的類(lèi)。如果沒(méi)有自定義類(lèi)加載器,應(yīng)用程序的類(lèi)默認(rèn)會(huì)被AppClassLoader加載。
  • 其他ClassLoader 需要用戶在特定的使用場(chǎng)景顯式使用。例如從URL加載class,或者說(shuō)是熱部署、模塊化框架、加密的類(lèi)資源等。這些classloader需要繼承ClassLoader類(lèi)。

Java的classLoader具有層級(jí)(父子)關(guān)系,classLoader按照雙親委派模型加載class。雙親委派模型后面說(shuō)明。上面所屬的四種classLoader,前面的classLoader是后面的父classLoader。

Java中所有的Class都有一個(gè)classLoader屬性,用來(lái)標(biāo)明該類(lèi)是由哪個(gè)類(lèi)加載器加載的。在程序中獲取一個(gè)類(lèi)由哪個(gè)類(lèi)加載器加載,可使用如下方式:

SomeKlass.class.getClassLoader();

下面舉一個(gè)例子:

public class ClassLoaderDemo {
    public static void main(String[] args) {
        System.out.println(String.class.getClassLoader());
        System.out.println(ClassLoaderDemo.class.getClassLoader());
    }
}

執(zhí)行的結(jié)果為:

null
sun.misc.Launcher$AppClassLoader@18b4aac2

如果一個(gè)類(lèi)由BootstrapClassLoader加載,那么該類(lèi)的classLoader屬性值為null。如果一個(gè)classloader的父classLoader為BootstrapClassLoader,這個(gè)classloader的parent屬性為null。(反過(guò)來(lái)也成立,null會(huì)被視為BootstrapClassLoader)

類(lèi)加載器行為

  • 加載class時(shí)默認(rèn)使用調(diào)用者的classLoader加載。
  • classloader自己不加載這個(gè)class,將加載工作轉(zhuǎn)交給父加載器去加載(如果父加載器還有父加載器,一直向上傳遞),如果父加載器找不到這個(gè)類(lèi)無(wú)法加載,子加載器才會(huì)嘗試加載。
  • 父加載器加載的class是會(huì)共享給所有的子加載器的(都算作已加載,子加載器的loadClass方法能夠獲取到父加載器加載的class)。但是多個(gè)平行關(guān)系的子加載器加載的class,互相之間不共享。
  • 決定兩個(gè)class是否相同的不僅是包名和class名,還有這個(gè)class的類(lèi)加載器。也就是說(shuō)如果包名和類(lèi)名相同的class被兩個(gè)不存在父子關(guān)系的類(lèi)加載器加載,那么加載后的這兩個(gè)class是完全不同的。

類(lèi)加載機(jī)制

  • 全盤(pán)負(fù)責(zé):當(dāng)一個(gè)類(lèi)加載器負(fù)責(zé)加載某個(gè)Class時(shí),該Class所依賴(lài)的和引用的其他Class也將由該類(lèi)加載器負(fù)責(zé)載入。除非顯式使用另外一個(gè)類(lèi)加載器來(lái)載入。
  • 父類(lèi)委托:任何類(lèi)加載器先讓父類(lèi)加載器試圖加載該類(lèi),只有在父類(lèi)加載器無(wú)法加載該類(lèi)時(shí)自己才嘗試從自身的類(lèi)路徑中加載該類(lèi)。
  • 緩存機(jī)制:緩存機(jī)制將會(huì)保證所有加載過(guò)的Class都會(huì)被緩存,當(dāng)程序中需要使用某個(gè)Class時(shí),類(lèi)加載器先從緩存區(qū)尋找該Class,只有緩存區(qū)不存在,系統(tǒng)才會(huì)讀取該類(lèi)對(duì)應(yīng)的二進(jìn)制數(shù)據(jù),并將其轉(zhuǎn)換成Class對(duì)象,存入緩存區(qū)。這種機(jī)制導(dǎo)致了class文件的修改無(wú)法實(shí)時(shí)在JVM中體現(xiàn)出來(lái)。如果需要實(shí)時(shí)體現(xiàn)出修改(稱(chēng)之為熱加載),需要放棄Class緩存然后使用類(lèi)加載器重新加載(例如再次新創(chuàng)建一個(gè)自定義加載器,重新加載該類(lèi))。

類(lèi)加載的方式

類(lèi)加載有三種方式:

1 啟動(dòng)應(yīng)用時(shí)候由JVM初始化加載
2 通過(guò)Class.forName()方法動(dòng)態(tài)加載
3 通過(guò)ClassLoader.loadClass()方法動(dòng)態(tài)加載

這三種方式的區(qū)別為:

  • 使用ClassLoader.loadClass()加載類(lèi),不會(huì)執(zhí)行初始化塊
  • 使用Class.forName()加載類(lèi),默認(rèn)會(huì)執(zhí)行初始化塊
  • 使用Class.forName(),指定類(lèi)加載器來(lái)加載類(lèi),不會(huì)執(zhí)行初始化塊

雙親委派模型

雙親委派模型的工作流程是:如果一個(gè)類(lèi)加載器收到了類(lèi)加載的請(qǐng)求,它首先不會(huì)自己去嘗試加載這個(gè)類(lèi),而是把請(qǐng)求委托給父加載器去完成,依次向上,因此,所有的類(lèi)加載請(qǐng)求最終都應(yīng)該被傳遞到頂層的啟動(dòng)類(lèi)加載器中,只有當(dāng)父加載器在它的搜索范圍中沒(méi)有找到所需的類(lèi)時(shí),即無(wú)法完成該加載,子加載器才會(huì)嘗試自己去加載該類(lèi)。

雙親委派模型目的是確保類(lèi)的全局唯一性。

ClassLoader類(lèi)的loadClass方法源代碼完整實(shí)現(xiàn)了雙親委派模型。代碼如下所示:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    // 加鎖,每個(gè)class對(duì)應(yīng)著不同的lock object
    // 確保同一個(gè)類(lèi)不會(huì)被同時(shí)加載
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        // 首先檢查這個(gè)class是否已經(jīng)加載過(guò)了
        // 如果已經(jīng)被加載過(guò),直接返回加載過(guò)的類(lèi)
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 如該加載器的父加載器存在,則調(diào)用父類(lèi)加載器的loadClass方法。
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // 如果parent為null,說(shuō)明父加載器為bootstrap類(lèi)加載器,查找并返回使用bootstrap類(lèi)加載器加載的類(lèi)
                    // 如果bootstrap沒(méi)有加載該類(lèi),返回null
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
            // 如果類(lèi)仍未加載(父類(lèi)加載器沒(méi)有加載,bootstrap類(lèi)加載器也沒(méi)有加載)
            // 則調(diào)用findClass方法,親自加載該類(lèi)
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                // findClass方法是詳細(xì)的根據(jù)類(lèi)名查找類(lèi)二進(jìn)制文件,讀取并解析的過(guò)程
                // findClass方法需要ClassLoader的子類(lèi)來(lái)重寫(xiě)
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

上面代碼的主要邏輯為:

  1. 檢查類(lèi)是否已經(jīng)被加載。如果已被加載,返回加載過(guò)的類(lèi)。
  2. 如果類(lèi)沒(méi)有被加載,交給該類(lèi)加載器的父加載器去加載。如果父加載器為BootStrapClassLoader,查找并返回它加載的類(lèi)。
  3. 如果父類(lèi)加載器無(wú)法加載該類(lèi)。自己再親自去加載這個(gè)類(lèi)。

Thread的contextClassLoader

用于使用父加載器加載的類(lèi)去顯式加載子加載器范圍內(nèi)的類(lèi)的情況,打破雙親委派模型。例如Java SPI。SPI的ServiceLoader::load代碼如下所示:

public final class ServiceLoader<S> implements Iterable<S> {
  
    public static <S> ServiceLoader<S> load(Class<S> service) {
        // 在Launcher類(lèi)的構(gòu)造器中被賦值為AppClassLoader
        // 可以讀取classpath
        // ServiceLoader自身默認(rèn)是BootStrap ClassLoader加載的,如果用這個(gè)class loader是無(wú)法讀取到用戶classpath中的類(lèi)的
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        // 這個(gè)方法中最終由指定classLoader的Class.forName方法去加載實(shí)現(xiàn)類(lèi)
        return ServiceLoader.load(service, cl);
    } 
}

從上面分析可知獲取線程context classLoader的方法為:

ClassLoader loader = Thread.currentThread().getContextClassLoader();

線程的contextClassLoader默認(rèn)為父線程的contextClassLoader。可以使用setContextClassLoader方法修改。

主要注意的是,contextClassLoader除非有意使用,否則永遠(yuǎn)不會(huì)被調(diào)用。加載class默認(rèn)使用的classLoader永遠(yuǎn)是調(diào)用者的classLoader

除此之外Context ClassLoader還可以用于線程間classLoader的共享和不同線程間classLoader的隔離。

自定義classLoader使用場(chǎng)景

自定義classLoader的幾個(gè)典型的使用場(chǎng)景:

  • 解決依賴(lài)沖突:使用不同的classLoader加載package和name相同的class,可以實(shí)現(xiàn)不同版本的class共存。
  • 熱加載:檢測(cè)二進(jìn)制文件變更(最后修改時(shí)間),檢測(cè)到變更之后創(chuàng)建新的classLoader然后加載這個(gè)class(不創(chuàng)建新的classLoader無(wú)法再次加載該class)。
  • class加密:對(duì)編譯之后的class文件二進(jìn)制內(nèi)容加密。使用時(shí)借助自定義classLoader讀取加密的二進(jìn)制內(nèi)容,解密后再交給JVM解析class。

編寫(xiě)自定義classLoader需要繼承ClassLoader類(lèi),重寫(xiě)findClass方法。自定義加載邏輯位于findClass方法中。獲取到Class內(nèi)容byte數(shù)組之后,將其傳遞給defineClass方法,解析為Java的Class。比如說(shuō)上面的class加密,可以在findClass的時(shí)候?qū)υ糲lass文件內(nèi)容解密之后再交給defineClass。

一個(gè)最簡(jiǎn)單的例子,使用自定義classLoader讀取項(xiàng)目編譯輸出目錄(使用maven在IDE里執(zhí)行對(duì)應(yīng)的是target/classes目錄)中指定名字的class二進(jìn)制文件,然后解析為Java的Class。

class MyClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) {
        String className = name.replace(".", "/").concat(".class");
        byte[] bytes;
        try (InputStream stream = getClass().getClassLoader().getResourceAsStream(className);
             ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            int i;
            while ((i = stream.read()) != -1) {
                outputStream.write(i);
            }
            bytes = outputStream.toByteArray();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        if (bytes == null) {
            throw new RuntimeException("Class not found");
        }

        return defineClass(name, bytes, 0, bytes.length);
    }
}

使用方式:

Class<?> aClass = myClassLoader.loadClass("org.example.ClassLoaderDemo");
Class<?> bClass = myClassLoader.findClass("org.example.ClassLoaderDemo");
System.out.println(aClass.getClassLoader());
System.out.println(bClass.getClassLoader());

輸出為:

sun.misc.Launcher$AppClassLoader@18b4aac2
org.example.MyClassLoader@6bc7c054

解釋?zhuān)?br> loadClass方法是用雙親委派模型加載,因?yàn)?code>ClassLoaderDemo類(lèi)位于classpath中,在啟動(dòng)的時(shí)候已經(jīng)被AppClassLoader加載過(guò)了。所以aClass的classLoader為AppClassLoaderloadClass方法間接調(diào)用了findClass方法。實(shí)際開(kāi)發(fā)中建議使用loadClass方法。
findClass方法是用戶自己實(shí)現(xiàn)的,如果直接調(diào)用的話沒(méi)有考慮雙親委派模型。這里為了演示直接調(diào)用。不建議在項(xiàng)目中直接使用。

參考文獻(xiàn)

https://zhuanlan.zhihu.com/p/51374915

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容