Java類加載機制

綜述

在前面兩篇文章主要分析了Java的內存以及Java的垃圾回收機制。在這篇文章中分析一下類是如何加載的。

類加載時機

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

對於開始類加載過程的第一個階段加載,Java虛擬機並沒有強制約束,可以交給虛擬機自由把握。但是對於初始化階段,虛擬機規範嚴格規定了有且只有5種情況必需立即對類進行“初始化”(而加載、驗證、準備需要再這之前開始)

  • 遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,如果類沒有進行過初始化,則需要先初始化。生成這4條指令最常見的Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一了類的靜態字段(被final修飾、已在編譯期把結果放到常量池的靜態字段除外)的時候,以及調用一個類的靜態方法。
  • 使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
  • 當初始化一個類的時候,如果發現其父類還沒有進行初始化,則需要先觸發其父類的初始化。
  • 當虛擬機啓動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
  • 當JDK1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果 REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先進行初始化。

注:當一個類在初始化時,要求其父類全部都已經初始化過了,但是一個接口在初始化的時,並不要求其父接口全部都完成了初始化,只有在真正使用到父類接口的時候(如引用父類定義的常量)纔會初始化。

類加載過程

加載
類的加載階段主要完成以下工作:

  • 通過一個類的全限定名來獲取定義此類的二進制流。
  • 將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時結構。
  • 在內存中生成一個代表這個類的java.lang.Class對象,作爲方法區這個類的各種數據的訪問入口

驗證
驗證是連接階段的第一步,這一階段爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。驗證階段大致會完成以下4個階段的檢驗動作:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證。

準備
準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。這時候進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一起分配在Java堆中。而”初始值“通常情況下是數據類型的零值。在一些特殊情況下,如果類字段的字段屬性表中存在ConstantValue屬性,那在準備階段變量value就會被初始化爲ConstantValue屬性所指定的值。
解析
虛擬機將常量池內的符號引用替換爲直接引用的過程。解析動作主要是針對類或者接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。“動態解析”的含義就是必須等到程序實際運行到這條指令的時候,解析動作才能進行。相對的,其餘可觸發解析的指令都是“靜態”的,可以在剛剛完成加載階段,還沒有開始執行代碼時就進行解析。
初始化
類的初始化階段是類加載的最後一步,在前面的類加載過程中除了加載階段是用戶應用程序可以通過自定義類加載器參與之外,其餘動作都是由虛擬機主導和控制。到了初始化階段,才真正開始執行類中定的Java字節碼。而初始化階段就是執行類構造器 < clinit >()方法的過程。< clinit >()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊中的語句合併產生的。< clinit >()與類的構造函數不同,它不需要顯示地調用父類構造器,虛擬機會保證在子類的< clinit >()方法執行之前,父類的< clinit >()方法已經執行完畢。

類加載器

類加載器分類

對於任何一個類,都需要由加載他的類加載器和這個類的本身一同確定其在Java虛擬機中的唯一性,每一個了類加載器,都擁有一個獨立的類名空間。也就是說比較兩個類是否”相等“
,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源於同一個Class文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必不相等。

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

如果劃分的更細緻一點,,絕大部分Java程序都會使用到以下3種系統提供的類加載器。

  • 啓動類加載器(Bootstrap ClassLoader): 由C++語言實現(針對HotSpot),負責將存放在\lib目錄或-Xbootclasspath參數指定的路徑中的類庫加載到內存中,即負責加載Java的核心類。
  • 擴展類加載器(Extension ClassLoader): 負責加載\lib\ext目錄或java.ext.dirs系統變量指定的路徑中的所有類庫,即負責加載Java擴展的核心類之外的類。
  • 應用程序類加載器(Application ClassLoader): 負責加載用戶類路徑(classpath)上的指定類庫,我們可以直接使用這個類加載器,通過ClassLoader.getSystemClassLoader()方法直接獲取。一般情況,如果我們沒有自定義類加載器默認就是用這個加載器。

雙親委派模型

雙親委派模型要求除了頂層的啓動類加載器以外,其餘的類加載器都應該有自己的父類加載器。這裏的類加載器之間的父子關係一般不會以繼承的關係來實現,而是都使用組合的關係來複用父加載器的代碼。

雙親委派模型的工作流程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把請求委託給父加載器去完成,依次向上,因此,所有的類加載請求最終都應該被傳遞到頂層的啓動類加載器中,只有當父加載器在它的搜索範圍中沒有找到所需的類時,即無法完成該加載,子加載器纔會嘗試自己去加載。

這樣的好處是不同層次的類加載器具有不同優先級,比如所有Java對象的超級父類java.lang.Object,位於rt.jar,無論哪個類加載器加載該類,最終都是由啓動類加載器進行加載,保證安全。即使用戶自己編寫一個java.lang.Object類並放入程序中,雖能正常編譯,但不會被加載運行,保證不會出現混亂。

雙親委派模型的代碼實現

ClassLoader中loadClass方法實現了雙親委派模型

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
        // 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);

                // this is the defining class loader; record the stats
            }
        }
        return c;
}

從上面代碼中可以看出,首先檢查該類是否已經被加載過,如果沒有被加載則調用父加載器的loadClass()方法,如果父加載器爲空則默認使用啓動類加載器作爲父加載器。如果父類加載失敗,拋出ClassNotFoundException異常後,在調用自己的findClass()方法進行加載。

總結

首先在類的加載過程中成中需要經歷加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)這7個階段。在使用類加載器的過程成,採用的是雙親委託模型對類進行加載。

參考周志明的《深入理解Java虛擬機》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章