天天看點

深入了解JVM總結——虛拟機類加載機制

虛拟機類加載機制

虛拟機把描述類的資料從class檔案加載到記憶體,并對資料進行校驗、轉換解析和初始化,最終形成可以直接被虛拟機使用的Java類型,這就是虛拟機的類加載機制。

什麼時候加載完成的

Java中,類的加載、連接配接和初始化過程都是在程式運作期間完成的。雖然會令類加載時稍微增加一些性能開銷,但會為Java應用程式提供高度的靈活性。Java可以動态擴充的語言特性就是依賴運作期動态加載和動态連接配接這個特點實作的。如面向接口的應用程式,可以等到運作時才指定其實際實作類。

對于Class檔案

在實際情況中,每個class檔案都可能代表Java語言中的一個類或接口。

文中, class檔案并非特指某個存在于具體磁盤中的檔案,應當是一串二進制的位元組流,無論以任何形式存在都可以。

類加載的時機

類從被加載到虛拟機記憶體開始,到解除安裝出記憶體為止,整個生命周期包括:加載,驗證,準備,解析,初始化,使用和解除安裝7個階段。其中,驗證,準備和解析3個階段被統稱為連接配接。

深入了解JVM總結——虛拟機類加載機制

其中,加載、驗證、準備、初始化和解除安裝這五個階段的順序是确定的,必須按照這個順序開始(不是進行或完成,因為這些階段通常是互相交叉地混合式進行的,通常會在一個階段執行的過程中調用激活另外一個階段)。而解析階段不一定:在某些情況下可以在初始化階段之後再開始,這是為了支援Java的運作時綁定(也稱為動态綁定或晚期綁定)。

虛拟機嚴格規範了有且隻有5種情況必須立即對類進行初始化(加載驗證準備已開始):

①遇到new ,getstatic,putstatic或invokestatic這4條位元組碼指令是,如果類沒有進行過初始化,則先需要觸發初始化。

—->場景:new執行個體化對象,讀取或設定一個類的靜态字段(被final修飾已在編譯期把結果放入常量池的靜态字段除外),調用一個類的靜态方法時。

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

③當初始化一個類的時候,如發現其父類還沒有初始化,則先觸發器父類的初始化。

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

⑤當使用JDK1.7的動态語言支援時,若一個java.lang.invoke.MethodHandle執行個體最後的解析結果REF_getStatic , REF_putStatic,REF_invokeStatic的方法句柄,且該方法句柄對應的類沒有進行過初始化,則需要先觸發其初始化。

“有且隻有這五種”“對一個類進行主動引用,其他的引用都不會觸發初始化”

對于接口來說,也存在初始化過程。接口中不能使用static{}語句塊,但編譯器會為就接口生成類構造器,用于初始化接口中定義的成員變量。接口與類的真正有差別的是有且僅有的第三個:接口初始化時,并不要求其父接口全部都完成了初始化,隻有在真正使用到父接口(如引用父接口定義中的常量)的時候才會初始化。

類加載的過程

加載驗證準備解析初始化。

加載

加載階段,虛拟機完成三件事:

①通過一個類的全限定名來擷取定義此類的二進制位元組流;

②将這個位元組流所代表的靜态存儲結構轉化為方法區的運作時資料結構;

③在記憶體中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種資料的通路入口。

–>非數組類的加載階段是可控性最強的,可以自定義類加載器去控制位元組流的擷取方式(即重寫一個類加載器的loadClass()方法)。

而數組類本身不通過類加載器創造,它是由Java虛拟機直接建立的,但它的元素類型最終是要靠類加載器去建立。

對于HotSpot而言,Class對象存在于方法區的。加載階段與連接配接階段的部分内容是交叉進行的,如驗證。

驗證

驗證,連接配接的第一步,目的是為了確定class檔案的位元組流中包含的資訊符合目前虛拟機的要求,且不會危害虛拟機自身安全。

class檔案并不一定要使用Java源碼來編譯,其他C#等都可以通過編譯器生成class檔案。

驗證直接決定了Java虛拟機是否能承受惡意代碼的攻擊,在類加載子系統中占據相當大的一部分。

驗證大緻完成4個階段的檢驗動作:

檔案格式驗證:是否以魔數開頭,主次版本号等..

中繼資料驗證:對位元組碼描述資訊進行分析,如是否有父類,是否抽象類等;

位元組碼驗證:最複雜的一個階段,主要目的是通過資料流和控制流分析,确定程式語義是合法的,合乎邏輯的。

符号引用驗證:發生在虛拟機将符号引用轉為直接引用的時候,在連接配接的第三個階段——解析是發生。符号引用通過字元串描述的全限定名是否能找到對應的類….

準備

準備是正式為類變量配置設定記憶體并設定類變量初始值的階段,這些變量所使用的記憶體都在方法區進行配置設定。

變量初始值通常情況下是資料類型的零值。若是常量,則初始值就為該數值。

public static int value=123; value準備階段初始值為0

public static final int value=123; value準備階段初始值為123

解析

解析是虛拟機将常量池中的符号引用替換為直接引用的過程。

符号引用:用一組符号來描述引用目标,符号可以是任何形式的字面量,隻要使用時無歧義即可。虛拟機實作的記憶體布局可以不相同,但所能接受的符号引用必須是一緻的,因為符号引用的字面量形式明确定義在Java虛拟機規範的class檔案格式中。

直接引用:可以直接指向目标的指針、相對偏移量或是一個能間接定位到目标的句柄。直接引用和虛拟機實作的記憶體布局相關的。如果直接引用存在,那麼引用的目标必定已經在記憶體中存在。

解析,主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符号引用進行(後三種與JDK1.7動态語言支援有關)。

初始化(非常重要,可以明白static等的加載順序)

類初始化階段是類加載過程的最後一步,前面的過程除了加載過程使用者可以自定義類加載器以外,其餘的動作完全是由虛拟機主導和控制。

到了初始化階段,才真正開始執行類中定義的Java程式代碼(或者說是位元組碼)。

初始化階段執行類構造器()方法的過程。

①()方法由編譯器自動收集類中所有類變量的指派動作和靜态語句塊中的語句合并産生的。定義在靜态語句塊後面的靜态變量,在靜态塊中可以指派,但不能通路。

public class Test{
    static{
        i=;//給變量指派,可以正常通過編譯
        System.out.println(i);//編譯器報錯,提示非法向前引用
    }
    static int i=;
}
           

②()方法與類的構造函數或執行個體構造器不同,不需顯示調用父類構造器,虛拟機會保證在子類類構造器方法執行之前,父類的類構造器方法已經執行完畢。是以在虛拟機中第一個被執行的()方法的類肯定是java.lang.Object。

③由于父類的類構造器方法先執行,也意味着父類中定義的靜态語句塊先于子類的變量指派操作。

④()方法對類或接口來說并不是必需的,如果類中無靜态語句塊,也沒有變量指派操作,就不會生成該方法;

⑤接口不能使用靜态語句塊,但仍舊有變量初始化指派操作。是以接口和類一樣也會生成()方法,但不需要先執行父接口的類構造器方法。隻有當父接口中的變量使用時,父接口才會初始化。同時,接口的實作類在初始化時也一樣不會執行接口的()方法。

⑥虛拟機會保證一個類的()方法在多線程環境中被正确的加鎖、同步。如果多線程同時去初始化一個類,那麼隻會有一個線程去執行該方法,其他的都需要阻塞等待,直到該方法執行完畢。若該方法耗時很久,就可能造成多個程序阻塞。阻塞往往是很隐蔽的。

同一個類加載器下,一個類型隻會初始化一次,是以阻塞之後其他線程不會再次進入()方法。

類加載器

類加載器,虛拟機加載階段“通過一個類的全限定名來擷取描述此類的二進制位元組流”這個動作放到Java虛拟機外部去實作,以便讓程式自己決定如何去擷取所需要的類,其中這個動作的代碼子產品就稱為“類加載器”。

最初是為滿足Java Applet的需求,目前浏覽器上該技術已“死掉”,但類層次劃分、OSGi、熱部署、代碼加密等,類加載器大放異彩。

類與類加載器

任意一個類都需要類加載器和類本身一同确立其在Java虛拟機中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。

比較兩個類是否相等,隻有在這兩個類來自同一個類加載器加載的情況下才可以比較是否相等,否則必定不相等。

雙親委派模型-JDK1.2引入

對Java虛拟機而言,隻存在兩種不同的類加載器:一種是啟動類加載器(Bootstrap ClassLoader),使用C++實作,虛拟機自身的一部分;還有一種是所有其他的類加載器,由Java實作,獨立于虛拟機外部,且全都繼承自抽象類java.lang.ClassLoader。

//自定義類加載器
ClassLoader myLoader=new ClassLoader(){
    //重寫loadClass函數
    @override
    public Class<?> loadClass(String name) throws Exception{
        .......
    }
};
           

對于開發人員來說有三種:

啟動類加載器,\lib中的或-Xbootclasspath指定路徑下且被虛拟機識别(如rt.jar,java.lang.Object在該類中)的類庫加載到虛拟機記憶體中。

擴充類加載器,\lib\ext中的或被java.ext.dirs系統變量所指定的路徑中的所有的類庫加載進虛拟機記憶體。

應用程式類加載器,由sum.misc.Launcher$AppClassLoader實作,該類加載器是ClassLoader的getSystemClassLoader()方法的傳回值,是以也稱為系統類加載器。負責加載使用者路徑classpath上所指定的類庫,可以直接使用。

深入了解JVM總結——虛拟機類加載機制

類加載器雙親委派模型,類加載器的層次關系,如圖所示。要求除了頂層的啟動類加載器以外,其餘的類加載器都應當有自己的父類加載器。它們之間的父子關系不是以繼承來實作的,而是都使用組合關系來複用父加載器的代碼。

一個類加載器收到了加載請求,首先不會自己去嘗試加載該類,而是把請求委派給父類加載器去完成,每一個層次的類加載器都是如此,是以所有的加載請求最終都傳到了頂層的啟動類加載器中,隻有父類加載器回報自己無法完成時,子類加載器才會嘗試自己去加載。

好處:Java類随着類加載器一起具備了一種帶有優先級的層次關系,例如java.lang.Object,存在于rt.jar中,任何類加載器都得加載這個類。

如果沒有使用這個模式的話,若使用者自行編寫java.lang.Object類,放在classpath中,系統将會出現多個不同的object類,會出現混亂。

雙親委派模型保證了Java程式的穩定運作,實作非常簡單。代碼集中在java.lang.ClassLoader的loadClass()方法中。

破壞雙親委派模型

雙親委派模型并非是一個強制性的限制模型。它很好的解決了各個類加載器的基礎類統一問題(越基礎的類由越上層的加載器進行加載)。

OSGi,Java子產品化标準,它實作子產品化熱部署的關鍵則是它自定義的類加載器機制的實作。每一個程式子產品(Bundle)都有一個自己的類加載器,當需要更換一個Bundle時,就把Bundle連同類加載器一起換掉以實作代碼的熱替換。