天天看點

類加載機制以及類加載器---Java虛拟機

文章目錄

    • 類加載機制以及類加載器---Java虛拟機
    • 1.類加載機制(Class Loading Mechanism):
      • 1.1加載(Loading)
      • 1.2驗證(Verification)
      • 1.3準備(Preparation)
      • 1.4解析(Resolution)
      • 1.5初始化(Initialization)
    • 2.類加載器(ClassLoader)
      • 2.1類加載器的種類(JDK1.8)
      • 2.2啟動類加載器(Bootstrap ClassLoader)
      • 2.3擴充類加載器(Extension ClassLoader)
      • 2.4 應用程式類加載器(Application ClassLoader)
      • 2.5 使用者自定義類加載器(UserDefined ClassLoader)
    • 3.雙親委派模型(Parent Delegation Model)以及其破壞
      • 3.1雙親委派模型
      • 3.2雙親委派模型的好處
      • 3.3 雙親委派模型的破壞

類加載機制以及類加載器—Java虛拟機

當編寫的一個java類檔案被編譯之後就變成了 類名.class 的位元組碼檔案.但是此時的.class檔案還不能馬上執行,隻有當其被加入到記憶體中的方法區,并對資料進行校驗,轉換解析和初始化之後,最終成為可以被java虛拟機直接使用的Java類型.這個過程就被稱為是Java虛拟機的類加載機制.下面我們通過兩張圖來形象的了解,類加載過程在整個JVM架構中的地位.
類加載機制以及類加載器---Java虛拟機

大家可以看到,JVM架構中有一個專門的類加載子系統來描述這個過程.這其中就有兩個部分值得大家好好了解

  1. 一個.class檔案是怎麼被加載進來的:這就是類加載機制
  2. 什麼東西負責将class檔案加載進來:這就是類加載器

下面就從下面這兩方面來深入了解一下類加載子系統.

1.類加載機制(Class Loading Mechanism):

在Java語言中,類型的加載,連接配接和初始化過程都是在程式的運作過程完成的,這樣雖然會讓java語言産生一點額外的開銷,但是卻為Java引用提供了極高的擴充性和靈活性,java天生可以動态擴充的語言特性就是依賴運作期動态加載和動态連接配接來實作的.

一個類型從被加載到虛拟機記憶體中開始,到解除安裝出記憶體為止,整個生命周期會經曆7個小階段:

  1. 加載(Loading)
  2. 驗證(Verification)
  3. 準備(Preparation)
  4. 解析(Resolution)
  5. 初始化(Initialization)
  6. 使用(Using)
  7. 解除安裝(Unloading)

其中驗證,準備,解析三個階段統稱為連接配接(Linking).其中加載,驗證,準備,初始化和解除安裝這5個過程的順序是确定的.下面是類的生命周期示意圖:

類加載機制以及類加載器---Java虛拟機

1.1加載(Loading)

加載(loading)是類加載(ClassLoading)的一個過程而已,大家不要搞混淆了.在加載(Loading)階段,java虛拟機需要完成以下三件事情:

  1. 通過一個類的全限定名來擷取定義該類的二進制位元組流檔案:例如大家常使用的String類的全限定名就是java.lang.String
  2. 将該位元組流所代表的靜态存儲結構轉換為方法區(Method Area)中的運作時資料結構:class檔案主要就是加載進了方法區
  3. 在記憶體中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種資料的通路入口(反射機制):通過反射機制就可以完成各種操作

1.2驗證(Verification)

驗證是連接配接階段的第一步,這一階段的目的是確定class位元組流中的資訊符合<java虛拟機規範>的全部要求,確定它運作後不會對JVM造成危害.

1.3準備(Preparation)

準備階段是正式為**類中定義的變量(用static修飾的靜态變量,也稱類變量)配置設定記憶體并設定零值(用final修飾的除外)的階段.而執行個體變量(也就是沒有用static修飾的變量)**會等到某個具體的對象被執行個體化的時候才會在java堆中配置設定.

類加載機制以及類加載器---Java虛拟機

例如:

private static int a=1;//準備階段配置設定記憶體,并且賦初始值0.初始化階段初始化為1
    private int b=2;//new 一個新對象的時候才在堆中配置設定記憶體并指派
    private final static int c=3;//準備階段就會配置設定記憶體,并且賦初始值3
           

也就是說類變量在準備階段不會按照代碼中初始值來指派,而是會等到初始化階段.但是如果用final來修飾就不一樣了.因為用final修飾的變量在javac進行編譯的時候就會為其生成一個ConstantValue屬性,于是在準備階段就會依照ConstantValue來指派

1.4解析(Resolution)

解析階段就是JVM将常量池中的符号引用替換為直接引用的過程.

1.5初始化(Initialization)

類的初始化時類加載過程中的最後一個動作.之前地介紹的幾個過程中除了在加載(Loading)階段可以通過使用者自定義加載器來局部參與以外,其餘的都是JVM在主導控制.直到初始化階段JVM才會真正開始執行程式員在類中編寫的Java程式代碼,将主導權交給應用程式.

在準備階段,類變量已經被賦了零值(常量除外),但是在初始化階段,就會根據程式員的編碼來初始化類變量以及其他資源.也可以從另一個更加直覺的角度來表達:初始化階段就是執行類構造器大的< clinit >()方法的過程.< clinit >()并不是由程式員編寫的,而是Javac自動收集類中的所有指派動作和靜态語句塊(static{})中的語句合并而成的.< clinit >()中執行順序就是語句在源檔案中出現的順序.

例如我們編寫一段代碼,并通過Jclasslib插件檢視位元組碼檔案:

public class ClinitTest
{
    private int a=0;
    private static int b=1;
    static
    {
        b=2;
    }
}
           

Jclasslib檢視結果如圖:

類加載機制以及類加載器---Java虛拟機

可以看到,按照順序先将b的值賦為1,再将b的值賦為2.沒有出現執行個體變量a的身影

下面還有幾個關于< clinit>方法的注意事項就介紹一下但是不示範了.

  • 在該類的< clinit >方法調用之前,JVM會保證父類的< clinit >一定已經執行完畢,也就是說Object類的< clinit >一定是最先執行的.
  • 一個類的< clinit >方法并不是必須的,如果一個類中沒有類變量的複制和靜态代碼塊的話,就不會為這個類生成< clinit >方法.
  • 接口中不能使用靜态代碼塊,但是可以聲明變量,是以也可以生成< clinit >方法,但是執行接口的< clinit >之前不會先執行父接口的< clinit >方法.而且接口的實作類在初始化的過程中也不會執行接口的< clinit >方法.
  • < clinit >會被正确的加鎖同步,因為一個類隻能被初始化一次.

2.類加載器(ClassLoader)

用來執行加載(Loading)階段中"通過一個類的全限定名來擷取描述該類的二進制位元組流"的動作的代碼就被稱為"類加載器"

類加載器結構示意圖:

類加載機制以及類加載器---Java虛拟機

很多人一看,類加載器隻是做了類加載階段一個階段中的一個步驟,是不能再小的一個功能呢,為什麼還要把它當做一個專門的部分來大書特書呢?

其實不然,它在java程式中起到的作用遠超類加載階段.對于任意一個類,都必須由加載它的類加載器和這個類本身來一起确定其在JVM中的唯一性,每一個類加載器都擁有一個獨立的類名稱空間.用更加通俗的語言表示的話:

判斷兩個類是否相等條件: (來自同一個class檔案&&被同一個類加載器加載)

2.1類加載器的種類(JDK1.8)

站在JVM的角度來看,隻存在兩大類類加載器:

  • 啟動類加載器(Bootstrap ClassLoader):用C++語言實作,是JVM自身的一個部分
  • 其他類加載器:由Java語言實作,獨立于虛拟機之外,繼承自抽象類java.lang.ClassLoader

但是站在java開發人員的角度來看,類加載器就分的更加詳細了,java一直保持着三層類加載器.雙親委派的類加載架構.

細緻的分類的話,可以分成4種,按照優先級排序如下,上面的加載器是下面的加載器的父類加載器

  1. 啟動類加載器(Bootstrap ClassLoader)
  2. 擴充類加載器(Extension ClassLoader)
  3. 應用程式類加載器(Application ClassLoader):又稱系統類加載器
  4. 使用者自定義類加載器(UserDefined ClassLoader)
類加載機制以及類加載器---Java虛拟機

2.2啟動類加載器(Bootstrap ClassLoader)

啟動類加載器是由c++代碼編寫的,主要用于加載:

  1. <JAVA_HOME>\lib中的核心類比如rt.jar,tools.jar中類
  2. 或者是通過JVM參數 VM Options:-Xbootclasspath參數所指定的類

結果是null,但是不意味着不存在,因為它是由C++編寫的,是以在Java層面無法表示出來,結果就是null.在JVM的類加載器中可以直接用null來指代Bootstrap ClassLoader.

2.3擴充類加載器(Extension ClassLoader)

這個類加載器是在類 sun.misc.Launcher$ExtClassLoader 中以java代碼實作的,它負責加載:

  1. <JAVA_HOME>\lib\ext目錄
  2. 或者被java.ext.dirs系統變量所指定的路徑中的所有類庫
ClassLoader classLoader = SunEC.class.getClassLoader();
        //結果是[email protected]
        System.out.println(classLoader);
           

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

這個類加載器是sun.misc.Launcher$AppClassLoader來實作的,它負責加載使用者類路徑(ClassPath)上的所有類庫,也就是使用者自己寫的類和第三方的類庫.

//springboot是第三方的類庫
        ClassLoader classLoader = SpringBootCondition.class.getClassLoader();
        //結果是[email protected]
        System.out.println(classLoader);

        //ClassLoaderTest是我自己定義的一個類類
        ClassLoader classLoader1 = new ClassLoaderTest().getClass().getClassLoader();
        //結果是[email protected]
        System.out.println(classLoader1);

        //通過ClassLoader.getSystemClassLoader()可以擷取ApplicationClassLoader
        ClassLoader ClassLoader3 = ClassLoader.getSystemClassLoader();
        //結果是[email protected]
        System.out.println(ClassLoader3);

           

又因為它是ClassLoader中靜态方法getSystemClassLoader()的傳回值,是以又稱"系統類加載器".

2.5 使用者自定義類加載器(UserDefined ClassLoader)

在預設情況下,java提供了3種類加載器,但是針對某些特殊情況,程式員可以定制屬于自己的類加載器,隻需要實作ClassLoader接口即可.

3.雙親委派模型(Parent Delegation Model)以及其破壞

3.1雙親委派模型

java中對類的加載采用的是動态加載,既隻要當使用到該類的時候才會去加載對應的位元組碼檔案.

當一個類加載器收到了類加載請求的時候并不會立馬就去加載,而是将這個請求委派給給它的父類加載器.直到到達頂層的Bootstrap加載器

如果父類加載器可以完成類的加載過程,那麼這個類的加載就交給父類完成.否則就會委派給其子類加載器.

下面是雙親委派模型的實作:

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        //1 首先檢查類是否被加載
        Class c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    //2 沒有則調用父類加載器的loadClass()方法;
                    c = parent.loadClass(name, false);
                } else {
                    //3 若父類加載器為空,則預設使用啟動類加載器作為父加載器;
                    c = findBootstrapClass0(name);
                }
            } catch (ClassNotFoundException e) {
                //4 若父類加載失敗,抛出ClassNotFoundException 異常後
                c = findClass(name);
            }
        }
        if (resolve) {
            //5 再調用自己的findClass() 方法。
            resolveClass(c);
        }
        return c;
    }
           

3.2雙親委派模型的好處

  1. 避免了類的重複加載:Java中的類和它的類加載器一起具備了一種帶有優先級的層次關系,例如java.lang.Object類是java中最核心類,無論哪一個類加載器要加載這個類,最終都會交給最頂端的Bootstrap類來加載,進而保證了不同環境下的Object都是同一個類(因為類相同有兩個方面).
  2. 保護程式安全,防止核心API被随意篡改:在程式設計中我們常常會使用第三方庫或者是自己定義的類,如果在程式中出現了一個全限定名為

    java.lang.Object的類,在沒有雙親委派模型的情況下,就會出現多個Object類,進而導緻程式混亂.

3.3 雙親委派模型的破壞

雙親委派模型并不是一個具有強制性限制的模型,而是Java設計者推薦給開發者的類加載器實作方式.在Java的世界中大部分的類加載器都遵循這個模型,但是也有例外的情況,直到java子產品化(JDK9)出現之前,它一共出現過3次較大規模的"被破壞"的情況

  1. 第一次"被破壞"出現在雙親委派模型出現(JDK1.2)之前,因為雙親委派模型在JDK1.2才被引入,但是ClassLoader在JDK1.1就已經存在.是以在這之前該模型是被破壞的.JDK1.2引入雙親委派模型之後,面對已經存在的使用者自定義的類加載器(主要是重寫loadClass方法),不得不做出一些妥協.因為雙親委派模型就是展現在loadClass()方法中,而JDK1.1的使用者都重寫了loadClass()方法,這樣就導緻了模型被破壞.是以設計者在java.lang.ClassLoader類中定義了一個新的protected 方法findClass().引導使用者編寫類加載機制的時候重寫該方法,就不會破壞模型.

    下面的代碼就是loadClass方法,它的邏輯就是雙親委派模型,大家可以看到第二個if(c==null) 的代碼處,當java的三個預設類加載器都沒有完成類的加載就會執行使用者自定義的findClass(),這樣既不影響使用者按照自己的意願去加載類,又不會破壞雙親委派模型.

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            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();
                    c = findClass(name);//程式員重寫findClass方法這樣就不會破壞模型

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

2.它的第二次破壞是由于它自身的缺陷所導緻的.雙親委派模型很好的解決了各個類加載器協作時基礎類型的一緻性問題(越基礎的類型由越上層的類加載器加載),但是如果基礎類型又需要調用使用者的代碼,那該怎麼辦呢?換句話說:程式員用自己的代碼實作了一個非常基礎的接口,也就是說這個非常基礎的接口肯定是由Bootstrap ClassLoader來加載的,但是類加載器卻不認識使用者的代碼?那該怎麼辦呢?

了解決這個困境,Java設計團隊隻好引入了一個不太優雅的設計:線程上下檔案類加載器(Thread Context ClassLoader)。這個類加載器可以通過java.lang.Thread類的setContextClassLoader()方法進行設定,如果建立線程時還未設定,它将會從父線程中繼承一個;如果在應用程式的全局範圍内都沒有設定過,那麼這個類加載器預設就是應用程式類加載器.也就是說進階的類加載器會借助線程上下檔案類加載器來加載使用者的代碼,這其實就已經打破了雙親委派模型.

類加載機制以及類加載器---Java虛拟機

3.雙親委派模型的第三次“被破壞”是由于使用者對程式的動态性的追求導緻的.IBM針對此推出了OSGi.

OSGi實作子產品化熱部署的核心在于它自定義的類加載器機制的實作。所有的程式子產品(Bundle)都有一個自己的類加載器,當需要更換一個Bundle時,就把Bundle連同類加載器一起換掉來實作代碼的熱替換。在OSGi幻境下,類加載器不再是雙親委派模型中的樹狀結構,而是進一步發展為更加複雜的網狀結構,當受到類加載請求時,OSGi将按照下面的順序進行類搜尋:

  1. 把java.*開頭的類委派給父類加載器加載。
  2. 不然,把委派清單名單内的類委派給父類加載器加載。
  3. 不然,把Import清單中的類委派給Export這個類的Bundle的類加載器加載。
  4. 不然,就查找目前Bundle的ClassPath,并使用自己的類加載器加載。
  5. 不然,就查找類是否在自己的Fragment Bundle中,如果在,則委派給Fragment Bundle的類加載器加載。
  6. 不然,就查找Dynamic Import清單的Bundle,委派給對應Bundle的類加載器加載。
  7. 不然,類加載器失敗。

碼字不易,覺得寫得不錯的小夥伴可以給個三連嗎?蟹蟹