天天看點

源碼剖析JVM類加載機制

1 前言

我們平常開發中,都會部署開發的項目或者本地運作main函數之類的來啟動程式,那麼我們項目中的類是如何被加載到JVM的,加載的機制和實作是什麼樣的,本文給大家簡單介紹下。

2 類加載運作全過程

當我們用java指令運作某個類的main函數啟動程式時,首先需要通過類加載器把主類加載到JVM,通過Java指令執行代碼的大體流程如下

源碼剖析JVM類加載機制

從流程圖中可以看到類加載的過程主要是通過類加載器來實作的,那麼什麼是類加載器呢?

3 類加載器

3.1 什麼是類加載器

類加載器負責在運作時将Java類動态加載到JVM(Java 虛拟機)。此外,它們是JRE(Java運作時環境)的一部分。是以由于類加載器,JVM不需要知道底層檔案或檔案系統來運作Java程式。

Java類加載器的作用是尋找類檔案,然後加載Class檔案加載到記憶體,并對資料進行校驗、轉換解析和初始化,最終形成可以被虛拟機直接使用的Java類型。

3.2 類加載器種類

3.2.1 啟動類加載器(Bootstrap ClassLoader)

它主要負責加載JDK内部類,一般是rt.jar和其他位于$JAVA_HOME/jre/lib目錄下的核心庫。此外,Bootstrap類加載器充當所有其他ClassLoader執行個體的父級。

Bootstrap ClassLoader是JVM核心的一部分,是用native引用編寫的。它本身是虛拟機的一部分,是以它并不是一個JAVA類,我們無法直接使用該類加載器。

3.2.2 擴充類加載器(Extension ClassLoader)

負責加載支撐JVM運作的位于$JAVA_HOME/jre/lib目錄下的ext擴充目錄中的JAR 類包。我們可以直接使用這個類加載器。

3.2.3 應用程式類加載器(Application ClassLoader)

負責加載使用者類路徑(classpath)上的指定類庫,主要就是加載你自己寫的那些類。一般情況,如果我們沒有自定義類加載器預設就是用這個加載器。

3.2.4 自定義類加載器

通過繼承ClassLoader類實作,主要重寫findClass方法。

下面通過代碼來看下了解不同的類是使用的哪種類加載器來加載的:

System.out.println("Classloader of this class : " + ClassLoaderDrill.class.getClassLoader());
System.out.println("Classloader of Logging : " + Logging.class.getClassLoader());
System.out.println("Classloader of String : " + String.class.getClassLoader());

System.out.println("-----------");

System.out.println("Classloader : " + ClassLoaderDrill.class.getClassLoader());
System.out.println("Classloader parent : " + ClassLoaderDrill.class.getClassLoader().getParent());
System.out.println("Classloader parent : " + ClassLoaderDrill.class.getClassLoader().getParent().getParent());           

下面是運作結果:

通過運作結果,我們會發現我自定義的目前運作類的類加載器是AppClassLoader,Logging這個類的類加載器是ExtClassLoader,而且類加載器之間是有父子關系關聯的。但String的類加載器卻為null,ExtClassLoader的父加載器也為null,是意味着String類不是通過類加載器加載的?那如果可以加載它又是怎麼被加載的呢?為什麼我們擷取不到BootstrapClassLoader呢?後面我們會進行解讀。

3.3 類加載器的機制

上面介紹了都有哪些類加載器,那麼一個類是如何被類加載器加載的,這些類加載器之間又有什麼關聯關系呢,接下來就介紹下類加載器的機制。

雙親委派機制

如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,是以所有的加載請求最終都會傳送到頂層的啟動類加載器中,隻有當父加載器回報自己無法完成這個加載請求時(它搜尋的範圍沒有找到所需的類),子加載器才會嘗試自己取加載。

雙親委派機制說簡單點就是:對于每個類加載器,隻有父類(依次遞歸)找不到時,才自己加載 。

源碼剖析JVM類加載機制

3.4 類加載機制的源碼實作

參見最開始類運作加載全過程圖可知,流程中會建立JVM啟動器執行個體:sun.misc.Launcher。 sun.misc.Launcher初始化使用了單例模式設計,保證一個JVM虛拟機内隻有一個sun.misc.Launcher執行個體。

在Launcher構造方法内部,其建立了兩個類加載器,分别是 sun.misc.Launcher.ExtClassLoader(擴充類加載器)和sun.misc.Launcher.AppClassLoader(應用類加載器)。JVM預設使用Launcher的getClassLoader(),這個方法傳回的類加載器(AppClassLoader)的執行個體加載我們的應用程式。

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);
        }

        Thread.currentThread().setContextClassLoader(this.loader);
        。。。。。。 //省略一些不需關注代碼
    }           

從上面Launcher構造方法的源碼中,我們看到了AppClassLoader和ExtClassLoader這兩種類加載器的定義,并且在建立AppClassLoader時将ExtClassLoader設定為父類,也符合上面說的類加載器之間的關聯。

但是BootstrapClassLoader仍然沒有出現,并且也沒有給ExtClassLoader設定父加載器,那它又是和ExtClassLoader如何關聯的?下面的雙親委派機制實作的源碼會為我們解答。

我們來看下AppClassLoader加載類的雙親委派機制源碼,AppClassLoader的loadClass方法最終會調用其父類ClassLoader的loadClass方法,該方法的大體邏輯如下:

  • 首先檢查一下指定名稱的類是否已經加載過,如果加載過了,就不需要再加載,直接傳回。
  • 如果此類沒有加載過,那麼,再判斷一下是否有父加載器;如果有父加載器,則由父加載器加載(即調用parent.loadClass(name, false);),或者是調用bootstrap類加載器來加載。
  • 如果父加載器及bootstrap類加載器都沒有找到指定的類,那麼調用目前類加載器的findClass方法,在檔案系統本身中查找類,來完成類加載。
  • 如果最後一個子類加載器也無法加載該類,則會抛出 java.lang.NoClassDefFoundError。
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 檢查目前類加載器是否已經加載了該類
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
  //如果目前加載器的父加載器不為空,則委托父加載器加載
                        c = parent.loadClass(name, false);
                    } else {
  //如果目前加載器父加載器為空,則委托啟動類加載器加載
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
// 都會調用URLClassLoader的findClass方法在加載器的類路徑裡查找并加載該類
                    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) {
 // 解析、連結指定的Java類
                resolveClass(c);
            }
            return c;
        }
    }           

上面就是雙親委派機制實作原理的源碼。從中我們可以看到有一個邏輯點會調用findBootstrapClassOrNull()這個方法,那麼至此,我們有個疑團也就解開了:ExtClassLoader和BootstrapClassLoader(啟動類加載器)就是在這裡關聯上的。因為ExtClassLoader在定義的時候,沒有設定父類加載器(parent),是以執行到了這個邏輯,委托了BootstrapClassLoader進行加載。上面說的類加載器之間層級關系的實作和關聯,也是在塊邏輯裡實作的。從源碼這裡的邏輯,也符合前面我們介紹BootstrapClassLoader所說的:Bootstrap類加載器充當所有其他ClassLoader執行個體的父級。

這個疑團是解開了,但是之前還有一個疑團仍然沒有說明,在開始我們擷取不同的類的加載器的時候,String的類加載器是null。在類加載的源碼裡面,我們看到了BootstrapClassLoader加載器的擷取,為什麼擷取不到是null呢。這個我們要看下findBootstrapClassOrNull()這個方法的實作,看看BootstrapClassLoader到底是怎麼定義的。

/**
     * Returns a class loaded by the bootstrap class loader;
     * or return null if not found.
     */
    private Class<?> findBootstrapClassOrNull(String name)
    {
        if (!checkName(name)) return null;

        return findBootstrapClass(name);
    }

    // return null if not found
    private native Class<?> findBootstrapClass(String name);           

通過源碼可以看到最終調用了findBootstrapClass這個方法來傳回,但是這個方法的修飾符是native,那麼就容易了解我們為什麼擷取不到這個BootstrapClassLoader了。

3.5 為什麼設計雙親委派

沙箱安全機制:自己寫的java.lang.String.class類不會被加載,這樣便可以防止核心API庫被随意篡改 ,防止了惡意代碼的注入,安全性的提高和保障。

避免類的重複加載:當父親已經加載了該類時,就沒有必要子ClassLoader再加載一次。如果每個加載器都自己加載,那麼可能會出現多個同名類,導緻混亂。

3.6 雙親委派機制的打破

雙親委派模型很好的解決了各個類加載器加載基礎類的統一性問題。即越基礎的類由越上層的加載器進行加載。

若加載的基礎類中需要回調使用者代碼,而這時頂層的類加載器無法識别這些使用者代碼時,就需要破壞雙親委派模型了。下面就介紹幾種破壞了雙親委派機制的場景。

3.6.1 JNDI破壞雙親委派模型

JNDI是Java标準服務,它的代碼由啟動類加載器去加載,但JNDI需要回調獨立廠商實作的代碼,而類加載器無法識别這些回調代碼(SPI)。為了解決這個問題,引入了一個線程上下文類加載器(ContextClassLoader)。可通過Thread.setContextClassLoader()設定。利用線程上下文類加載器去加載所需要的SPI代碼,即父類加載器請求子類加載器去完成類加載的過程,而破壞了雙親委派模型。

3.6.2 Spring破壞雙親委派模型

Spring要對使用者程式進行組織和管理,而使用者程式一般放在WEB-INF目錄下,由WebAppClassLoader類加載器加載,而Spring由Common類加載器或Shared類加載器加載。

那麼Spring是如何通路WEB-INF下的使用者程式呢?——使用線程上下文類加載器

Spring加載類所用的classLoader都是通過Thread.currentThread().getContextClassLoader()擷取的。當線程建立時會預設建立一個AppClassLoader類加載器(對應Tomcat中的WebAppclassLoader類加載器): setContextClassLoader(AppClassLoader)。利用這個來加載使用者程式,即任何一個線程都可通過getContextClassLoader()擷取到WebAppclassLoader。

3.6.3 Tomcat破壞雙親委派機制

  • 不同的應用程式可能會依賴同一個第三方類庫的不同版本,不能要求同一個類庫在同一個伺服器隻有一份,是以要保證每個應用程式的類庫都是獨立的,保證互相隔離。
  • 部署在同一個web容器中相同的類庫相同的版本可以共享
  • web容器也有自己依賴的類庫,不能與應用程式的類庫混淆。
  • web容器要支援jsp的修改,需要支援 jsp 修改後不用重新開機。
源碼剖析JVM類加載機制

3.7 自定義類加載器

在介紹類加載器種類的時候,一共有四種,前面所說的都是前三種類加載器的一些機制,那如果我們想自己自定義個類加載器要如何實作呢?

自定義類加載器,隻需繼承ClassLoader抽象類,并重寫findClass方法(如果要打破雙親委派模型,需要重寫loadClass方法)。下面是個自定義類加載器的例子:

public class ClassLoaderDrill {
    static class MyClassLoader extends ClassLoader {
        private String classPath;

        public MyClassLoader(String classPath) {
            this.classPath = classPath;
        }

        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                //defineClass将一個位元組數組轉為Class對象,這個位元組數組是class檔案讀取後最終的位元組 數組。
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }

        private byte[] loadByte(String name) throws Exception {
            name = name.replaceAll("\\.", "/");
            FileInputStream fis = new FileInputStream(classPath + "/" + name
                    + ".class");
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;
        }
    }


    public static void main(String args[]) throws Exception {
        //初始化自定義類加載器,會先初始化父類ClassLoader,其中會把自定義類加載器的父加載器設定為應用程式類加載器AppClassLoader
        MyClassLoader classLoader = new MyClassLoader("D:/test");
        //建立 /com/xxx/xxx 的幾級目錄,跟你要加載類的目錄一緻
        Class clazz = classLoader.loadClass("com.test.jvm.User");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("sout", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
}           

注意:一個ClassLoader建立時如果沒有指定parent,那麼它的parent預設就是AppClassLoader。這個在ClassLoader的構造方法實作裡可以看到。

4 類加載的過程

上述我們介紹了類加載器及相關機制和實作源碼,但是類加載器擷取所需要的類這個動作,隻是類加載全過程中的一部分。類從被加載到虛拟機記憶體中開始,到解除安裝出記憶體為止,它的整個生命周期包括:加載、驗證、準備、解析、初始化、使用和解除安裝7個階段。其中驗證、準備、解析三個部分統稱為連結,這7個階段的發生順序如圖:

源碼剖析JVM類加載機制

下面也給大家簡單介紹下每個階段所執行的具體動作

4.1 加載

JVM 在該階段的主要目的是将位元組碼從不同的資料源(可能是 class 檔案、也可能是 jar 包,甚至網絡)轉化為二進制位元組流加載到記憶體(JVM)中,并生成一個代表該類的 java.lang.Class 對象。該階段JVM完成3件事:

  • 通過類的全限定名擷取該類的二進制位元組流(需要特别說明的是我們上述所說的類加載器相關動作,就是類加載過程中的這個階段)
  • 将位元組流所代表的靜态存儲結構轉化為方法區的運作時資料結構
  • 在記憶體中生成一個該類的java.lang.Class對象,作為該類在方法區的各種資料的通路入口

4.2 驗證

主要確定加載進來的位元組流符合JVM規範。JVM 會在該階段對二進制位元組流進行校驗,隻有符合 JVM 位元組碼規範的才能被 JVM 正确執行,該階段是保證 JVM 安全的重要屏障。

驗證階段會完成以下4個階段的檢驗動作:

  • 檔案格式驗證:基于位元組流驗證
  • 中繼資料驗證(是否符合Java語言規範):基于方法區的存儲結構驗證
  • 位元組碼驗證(确定程式語義合法,符合邏輯):基于方法區的存儲結構驗證
  • 符号引用驗證(確定下一步的解析能正常執行):基于方法區的存儲結構驗證

4.3 準備

該步主要為靜态變量在方法區配置設定記憶體,并設定預設初始值。JVM 會在該階段對類變量(也稱為靜态變量,static 關鍵字修飾的變量)配置設定記憶體并初始化。

4.4 解析

虛拟機将常量池内的符号引用替換為直接引用的過程,即将常量池中的符号引用轉化為直接引用。

4.5 初始化

在準備階段,類變量已經被賦過預設初始值,而在初始化階段,類變量将被指派為代碼期望賦的值。換句話說,初始化階段是執行類構造器方法的過程。

4.6 使用

使用階段包括主動引用和被動引用,主動飲用會引起類的初始化,而被動引用不會引起類的初始化。當使用階段完成之後,java類就進入了解除安裝階段。

4.7 解除安裝

關于類的解除安裝,在類使用完之後,如果滿足下面的情況,jvm就會在方法區垃圾回收的時候對類進行解除安裝。類的解除安裝過程其實就是在方法區中清空類資訊,java類的整個生命周期就結束了。

  • 該類所有的執行個體都已經被回收,也就是java堆中不存在該類的任何執行個體。
  • 加載該類的ClassLoader已經被回收。
  • 該類對應的java.lang.Class對象沒有任何地方被引用,無法在任何地方通過反射通路該類的方法。

5 總結

最後介紹了下類加載的整個過程及執行的具體動作,其實每個節點去深挖也是有很多内容的,感興趣的小夥伴可以再去深入了解。

作者:京東物流 孫靖凱

繼續閱讀