Java虛擬機類加載機制

虛擬機把描述類的數據從Class文件加載到內存,并對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。

在Java語言里,類型的加載、連接和初始化過程都是在程序運行期間完成的,這種策略雖然會令類加載時稍微增加一些性能開銷,但是會為Java應用程序提供高度的靈活性,Java的動態擴展特性就是依賴運行期動態加載和動態連接這個特點實現的。

類加載的時機

類從被加載到虛擬機內存中開始,到卸載出內存為止,它的整個生命周期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。其中驗證、準備、解析3個部分統稱為連接(Linking)。這7個階段發生的順序如圖所示:

類的生命周期

加載、驗證、準備、初始化和卸載這5個階段的順序是確定的,類的加載過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之后再開始,這是為了支持Java語言的運行時綁定。

按部就班地“開始”,強調這點是因為這些階段通常都是互相交叉地混合式進行的,通常會在這一階段執行的過程中調用、激活另外一個階段。

什么情況下需要開始類加載過程的第一個階段:加載?Java虛擬機規范中并沒有進行強制約束,這點可以交給虛擬機的具體實現來把握。但對于初始化階段,虛擬機規范有嚴格的規定,有且只有5種情況必須立即對類進行“初始化”

1、 遇到new、getstatic、setstatic或invokestatic這4條字節碼指令時,如果類沒有進行初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態子段(被final修飾、已在編譯期把結果放入常量池的靜態子段除外)的時候,以及調用一個類的靜態方法的時機。

2、 使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。

3、當初始化一個類的時候,如果發現其父類還沒有進行初始化,則需要先觸發其父類的初始化。

4、當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。

5、當使用JDK 1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結果
REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個方法句柄所對應的類沒有進行初始化,則需要先觸發其初始化。

這5種場景中的行為稱為對一個類進行主動引用,除此之外,所有引用類的方法都不會出發初始化,稱為被動引用。

接口也有初始化過程,但是一個接口在初始化時,并不要要求其父接口全部都完成了初始化,只有在真正使用到父接口的時候(如引用接口中定義的常量)才會初始化。

類加載的過程

Java虛擬機類加載的全過程是加載、驗證、準備、解析和初始化這5個階段所執行的具體動作。

加載

在加載階段,虛擬機需要完成以下3件事情:

1)通過一個類的全限定名來獲取定義此類的二進制字節流;

2)將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構;

3)在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口;

數組類本身不通過類加載器創建,它是由Java虛擬機直接創建的。但數組類與類加載器仍然有很密切的關系,因為數組類的元素類型最終是要靠類加載器去創建。

驗證

驗證是連接階段的第一步,這一階段的目的是為了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。

從整體上來看,驗證階段大致上會完成下面4個階段的校驗動作:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證。

準備

準備階段正式為類變量分配內存并設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。首先,這個時候進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨著對象一起分配在Java堆中;其次,這里所說的初始值“通常情況”下是數據類型的零值。

如果類子段的字段屬性表中存在ConstantValue屬性,那在準備階段變量value就會被初始化為ConstantValue屬性所指定的值。

解析

解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程。

  • 符號引用

    符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時無歧義地定位到目標即可。符號引用與虛擬機實現的內存布局無關,引用的目標并不一定已經加載到內存中。各種虛擬機實現的內存布局可以各不相同,但是它們能接受的符號引用必須都是一致的,因為符號引用的字面量形式明確定義在Java虛擬機規范的Class文件格式中。

  • 直接引用

    直接引用可以是直接指向目標的指針、相對偏移量或一個能間接定位到目標的句柄。直接引用是和虛擬機實現的內存布局相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經在內存中存在。

初始化

在初始化階段,才真正開始執行類中定義的Java程序代碼,根據程序員通過程序制定的主觀計劃去初始化類變量和其他資源?;蛘呖梢詮牧硗庖粋€角度來表達:初始化階段是執行類構造器<clinit>()方法的過程。

<clinit>()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊中的語句合并產生的,編輯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變量,定義在它之后的變量可以賦值,但不能訪問。

<clinit>()方法執行過程中一些可能會影響程序運行行為的特點和細節:

1、<clinit>()方法與類的構造函數不同,它不需要顯式地調用父類構造函數,虛擬機會保證在子類的<clinit>()方法執行之前,父類的<clinit>()方法已經執行完畢。因此在虛擬機中第一個被執行的<clinit>()方法的類肯定是java.lang.Object。

2、<clinit>()方法對類和接口來說并不是必需的,如果一個類中沒有靜態語句塊,也沒有對變量的賦值操作,那么編譯器可以不為這個類生成<clinit>()方法。

3、接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生成<clinit>()方法。但接口與類不同的是,執行接口的<clinit>()方法不需要先執行父接口的<clinit>()方法,只有當父接口中定義的使用時,父接口才會初始化。另外,接口的實現類在初始化也一樣不會執行接口的<clinit>()方法。

4、虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確地加鎖、同步,如果多個線程同時去初始化一個類,那么只會有一個線程去執行這個類的<clinit>()方法,其他線程都需要阻塞等待,直到活動線程執行<clinit>()方法完畢。

類加載器

虛擬機設計團隊把類加載階段中的“通過一個類的全限定名來獲取描述此類的二進制字節流”這個動作放到Java虛擬機外部去實現,以便讓應用程序自己決定如何去獲取所需要的類。實現這個動作的代碼模塊稱為“類加載器”。

類與類加載器

類加載器雖然只用于實現類的加載動作,但它在Java程序中起到的作用遠遠不限于類加載階段。對于任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。

比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源于同一個Class文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等。

雙親委派模型

從Java虛擬機的角度來講,只存在兩種不同的類加載器:一種是啟動加載器(Bootstrap ClassLoader),這個類加載器使用C++語言,是虛擬機自身的一部分;另外一種就是所有其他的類加載器,這些類加載器都由Java語言實現,獨立于虛擬機外部,并且全部都繼承自抽象類java.lang.ClassLoader。

從Java開發人員的角度來看,絕大部分Java程序都會使用到以下3種系統提供的類加載器。

  • 啟動類加載器(Bootstrap ClassLoader)

    負責將存在${JAVA_HOME}\lib目錄中的,或者是被-Xbootclasspath所指定的路徑中,并且是虛擬機識別的類庫加載到虛擬機內存中。

    啟動類加載器無法被Java程序直接引用。

  • 擴展類加載器(Extension Classloader)

    負責加載${JAVA_HOME}\lib\ext目錄中的,或者是被java.ext.dirs系統變量所指定的所有類庫。

  • 應用程序加載器(Application Classloader)

    負責加載用戶類路徑(classpath)上所指定的類庫,如果應用程序沒有子定義過自己的類加載器,一般情況下這個是程序中默認的類加載器。

類加載器的雙親委派模型并不是一個強制性的約束模型,而是Java設計者推薦給開發者的一種類加載器實現方式。其工作過程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中,只有當父加載器反饋自己無法完成這個加載請求時,子加載器才會嘗試自己去加載。

實現雙親委派的代碼都集中在java.lang.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 {
                // 如果存在父加載器,則委托其父加載器加載請求的類;否則由啟動類加載器加載。
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                
            }
            if (c == null) {
                long t1 = System.nanoTime();
                // 在父類無法加載的時候,再調用本身的findClass方法來進行加載
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

破壞雙親委派模型

在Java的世界中大部分的類加載器都遵循這個模型,但也有例外,到目前為止,雙親委派模型主要出現過3較大規模的“被破壞”情況:

1、雙親委派模型在JDK 1.2之后才被引入,而類加載器和抽象類java.lang.ClassLoader則在JDK 1.0時代就已經存在,面對已經存在的用戶自定義類加載的實現代碼,Java設計者引入雙親模型時不得不做一些妥協。在此之前,用戶繼承java.lang.ClassLoader的唯一目的就是為了重寫loadClass()方法,為了向前兼容,JDK 1.2之后的java.lang.ClassLoader添加了一個新的protected方法findClass(),推薦把自己的類加載邏輯寫到findClass()方法中。

2、雙親委派模型很好地解決了各個類加載器的基礎類的統一問題,之所以稱為“基礎類”,是因為它們總是作為被用戶代碼調用的API。如果基礎類又要調用用戶的代碼呢,典型的例子就是JNDI服務。為了解決這個問題,Java設計團隊引入一個不太優雅的設計:線程上下文類加載器。線程上下文加載器可以通過java.lang.Thread類的setContextClassLoader()方法進行設置,如果創建線程時還未設置,它將會從父線程中繼承一個,如果在應用程序的全局范圍內都沒有設置過的話,那這個類加載器默認就是應用程序類加載器。

3、在OSGi環境下,類加載器不再是雙親委派模型中的樹狀結構,而是進一步發展為更加復雜的網狀結構。

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

推薦閱讀更多精彩內容

  • 讀書筆記 深入理解Java虛擬機:JVM高級特性與最佳實現(第二版) 概述 深入了解了Class文件存儲格式的具...
    Bollen_Chak閱讀 487評論 0 1
  • 什么叫類加載 JVM 將Class文件加載到內存,并對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用...
    那些年未曾努力過閱讀 417評論 0 0
  • Java程序運行于Java虛擬機之上,JVM屏蔽了底層細節,使得Java程序能夠“一次編譯,到處運行”。在Java...
    程序之心閱讀 334評論 0 8
  • Java類加載的過程 類從被加載到虛擬機內存中開始,到卸載出內存為止,它的整個生命周期包括:加載、驗證、準備、解析...
    hbh404閱讀 303評論 0 0
  • 虛擬機把描述類的數據從Class文件加載到內存,并對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的...
    云飛揚1閱讀 1,621評論 2 51