深刻理解虛擬機之虛擬機類加載機制

《深刻理解Java虛擬機:JVM高級特性與最佳實踐(第二版》讀書筆記與常見相關面試題總結java

本節常見面試題(推薦帶着問題閱讀,問題答案在文中都有提到):面試

簡單說說類加載過程,裏面執行了哪些操做?編程

對類加載器有了解嗎?數組

什麼是雙親委派模型?緩存

雙親委派模型的工做過程以及使用它的好處。安全

前言:

代碼編譯的結果從本地轉換爲字節碼,是存儲格式發展的一小步,倒是編程語言發展的一大步。微信

1 概述

上一節咱們已經知道了類文件結構,在class文件中描述的各類信息最終都須要加載到虛擬機中以後才能運行和使用。網絡

那麼虛擬機是如加載這些class文件呢?class文件中的信息進入到虛擬機後會發生什麼變化呢?數據結構

1.1 虛擬機類加載機制的概念

虛擬機把描述類的數據從class文件加載到內存,並對數據進行校驗、轉換解析和初始化。最終造成能夠被虛擬機最直接使用的java類型的過程就是虛擬機的類加載機制。編程語言

1.2 Java語言的動態加載和動態鏈接

另外須要注意的很重要的一點是:java語言中類型的加載鏈接以及初始化過程都是在程序運行期間完成的,這種策略雖然會使類加載時稍微增長一些性能開銷,可是會爲java應用程序提供高度的靈活性。java裏天生就能夠動態擴展語言特性就是依賴運行期間動態加載和動態鏈接這個特色實現的。好比,若是編寫一個面向接口的程序,能夠等到運行時再指定其具體實現類。

2 類加載時機

類從被加載到虛擬機內存到卸出內存爲止,它的整個生命週期包括:
類的生命週期
咱們思考一下那麼何時須要開始類加載的第一個階段:加載?

虛擬機規範嚴格規定了有且只有五種狀況必須當即對類進行「初始化」:

  • 使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段的時候,已經調用一個類的靜態方法的時候。
  • 使用java.lang.reflect包的方法對類進行反射調用的時候,若是類沒有初始化,則須要先觸發其初始化。
  • 當初始化一個類的時候,若是發現其父類沒有被初始化就會先初始化它的父類。
  • 當虛擬機啓動的時候,用戶須要指定一個要執行的主類(就是包含main()方法的那個類),虛擬機會先初始化這個類;
  • 使用Jdk1.7動態語言支持的時候的一些狀況。

而對於接口,當一個接口在初始化時,並不要求其父接口所有都完成了初始化,只有在真正使用到父接口時(如引用父接口中定義的常量)纔會初始化。

全部引用類的方式都不會觸發初始化稱爲被動引用,下面是3個被動引用例子:

①經過子類引用父類靜態字段,不會致使子類初始化;②經過數組定義引用類,不會觸發此類的初始化

public class SuperClass {
    static {
        System.out.println("SuperClass(父類)被初始化了。。。");
    }
    public static int value = 66;
}
public class Subclass extends SuperClass {
    static {
        System.out.println("Subclass(子類)被初始化了。。。");

    }
    
}
public class Test1 {

    public static void main(String[] args) {

        // 1:經過子類調用父類的靜態字段不會致使子類初始化
        // System.out.println(Subclass.value);//SuperClass(父類)被初始化了。。。66
        // 2:經過數組定義引用類,不會觸發此類的初始化
        SuperClass[] superClasses = new SuperClass[3];
        // 3:經過new 建立對象,能夠實現類初始化,必須把1下面的代碼註釋掉纔有效果否則通過1的時候類已經初始化了,下面這條語句也就沒用了。
        //SuperClass superClass = new SuperClass();
    }

}

③常量在編譯階段會存入調用類的常量池中,本質上並無直接引用定義常量的類,所以不會觸發定義常量的類的初始化

public class ConstClass {
    static {
        System.out.println("ConstClass被初始化了。。。");
    }
    public static final String HELLO = "hello world";
}
public class Test2 {

    public static void main(String[] args) {
        System.out.println(ConstClass.HELLO);//輸出結果:hello world
    }

}

3 類加載過程

下面咱們詳細的說一下java虛擬機中類加載的全過程:加載驗證準備解析初始化這5個階段鎖執行的具體工做。

3.1 加載

「加載」「類加載」 過程的一個階段,切不可將兩者混淆。

加載階段由三個基本動做組成:

1) 經過類型的徹底限定名,產生一個表明該類型的二進制數據流(根本沒有指明從哪裏獲取、怎樣獲取,能夠說一個很是開放的平臺了)

2) 解析這個二進制數據流爲方法區內的運行時數據結構

3) 建立一個表示該類型的java.lang.Class類的實例,做爲方法區這個類的各類數據的訪問入口。

經過類型的徹底限定名,產生一個表明該類型的二進制數據流的幾種常見形式:

  • 從zip包中讀取,成爲往後JAR、EAR、WAR格式的基礎;
  • 從網絡中獲取,這種場景最典型的應用就是Applet;
  • 運行時計算生成,這種場景最經常使用的就是動態代理技術了;
  • 由其餘文件生成,好比咱們的JSP;

注意: 非數組類加載階段既可使用系統提供的類加載器來完成,也能夠由用戶自定義的類加載器去完成。(即重寫一個類加載器的loadClass()方法)

3.2 驗證

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

虛擬機若是不檢查輸入的字節流,並對其徹底信任的話,極可能會由於載入了有害的字節流而致使系統崩潰,因此驗證是虛擬機對自身保護的一項重要工做。這個階段是否嚴謹,直接決定了java虛擬機是否能承受惡意代碼的攻擊。

從總體上看,驗證階段大體上會完成4個階段的校驗工做:文件格式、元數據、字節碼、符號引用

3.2.1 文件格式驗證

驗證字節流是否符合Class文件格式的規範,而且能被當前版本的虛擬機處理。該驗證階段的主要目的是保證輸入的字節流能正確地解析並存儲於方法區以內。這個階段驗證是基於二進制字節流進行的,只有經過這個階段的驗證後,字節流纔會進入內存的方法區進行存儲,因此後面的3個階段的所有是基於方法區的存儲結構進行的,不會再直接操做字節流。

3.2.2 元數據驗證

該階段對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範的要求,目的是保證不存在不符合Java語言規範的元數據信息

3.2.3 字節碼驗證

該階段主要工做時進行數據流和控制流分析,保證被校驗類的方法在運行時不會作出危害虛擬機安全的行爲。 例如,保證跳轉指令不會跳轉到方法體之外的字節碼指令上、保證方法體中的類型轉換是有效的等等。

因爲數據流校驗的高複雜性,耗時較大,因此JDK1.6以後,在Javac中引入一項優化方法(能夠經過參數關閉):在方法體的Code屬性的屬性表中增長一項「StackMapTable」屬性,該屬性描述了方法體中全部基本塊開始時本地變量表和操做棧應有的狀態,從而將字節碼驗證的類型推導轉變爲類型檢查從而節省一些時間。

注意: 若是一個方法體經過了字節碼驗證,也不能說明其必定是安全的,由於校驗程序邏輯沒法作到絕對精確。

3.2.4 符號引用驗證

最後一個階段的校驗發生在虛擬機將符號引用轉化爲直接引用的時候,這個轉化動做將在鏈接的第三個階段——解析階段中發生。符號引用驗證的目的是確保解析動做能正常執行。

驗證的內容主要有:

  • 符號引用中經過字符串描述的全限定名是否能找到對應的類;
  • 在指定類中是否存在符號方法的字段描述及簡單名稱所描述的方法和字段;
  • 符號引用中的類、字段和方法的訪問性(private、protected、public、default)是否可被當前類訪問。

3.3 準備

準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。(備註:這時候進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一塊兒分配在Java堆中)。

初始值一般是數據類型的零值:

對於:public static int value = 123;,那麼變量value在準備階段事後的初始值爲0而不是123,這時候還沒有開始執行任何java方法,把value賦值爲123的動做將在初始化階段纔會被執行。

一些特殊狀況:

對於:public static final int value = 123;編譯時Javac將會爲value生成ConstantValue屬性,在準備階段虛擬機就會根據ConstantValue的設置將value賦值爲123。

基本數據類型的零值:
基本數據類型的零值

3.4 解析

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

那麼符號引用與直接引用有什麼關聯呢?

3.4.1 看二者的概念。

符號引用(Symbolic References): 符號引用以一組符號來描述所引用的目標,符號能夠是符合約定的任何形式的字面量,符號引用與虛擬機實現的內存佈局無關,引用的目標並不必定已經加載到內存中。

直接引用(Direct References): 直接引用能夠是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用與虛擬機實現的內存佈局相關,引用的目標一定已經在內存中存在。

虛擬機規範沒有規定解析階段發生的具體時間,虛擬機實現能夠根據須要來判斷究竟是在類被加載時解析仍是等到一個符號引用將要被使用前纔去解析。

3.4.2 對解析結果進行緩存

同一符號引用進行屢次解析請求是很常見的,除invokedynamic指令之外,虛擬機實現能夠對第一次解析結果進行緩存,來避免解析動做重複進行。不管是否真正執行了屢次解析動做,虛擬機須要保證的是在同一個實體中,若是一個引用符號以前已經被成功解析過,那麼後續的引用解析請求就應當一直成功;一樣的,若是 第一次解析失敗,那麼其餘指令對這個符號的解析請求也應該收到相同的異常。

3.4.3 解析動做的目標

解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。前面四種引用的解析過程,對於後面三種,與JDK1.7新增的動態語言支持息息相關,因爲java語言是一門靜態類型語言,所以沒有介紹invokedynamic指令的語義以前,沒有辦法將他們和如今的java語言對應上。

3.5 初始化

類初始化階段是類加載的最後一步,前面的類加載過程當中,除了在加載階段用戶應用程序能夠經過自定義類加載器參與以外,其他動做徹底由虛擬機主導和控制。到了初始化階段,才真正開始執行類中定義的java程序代碼(或者說是字節碼)。

4 類加載器

4.一、類與類加載器

對於任意一個類,都須要由加載它的類加載器和這個類自己一同確立其在Java虛擬機中的惟一性。若是兩個類來源於同一個Class文件,只要加載它們的類加載器不一樣,那麼這兩個類就一定不相等。

4.2 類加載器介紹

從Java虛擬機的角度分爲兩種不一樣的類加載器:啓動類加載器(Bootstrap ClassLoader)其餘類加載器。其中啓動類加載器,使用C++語言實現,是虛擬機自身的一部分;其他的類加載器都由Java語言實現,獨立於虛擬機以外,而且全都繼承自java.lang.ClassLoader類。(這裏只限於HotSpot虛擬機)。

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

啓動類加載器(Bootstrap ClassLoader):

這個類加載器負責將存放在<JAVA_HOME>lib目錄中的,或者被-Xbootclasspath參數所指定的路徑中的,而且是虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即便放在lib目錄中也不會被加載)類庫加載到虛擬機內存中。

擴展類加載器(Extension ClassLoader):

這個加載器由sun.misc.Launcher$ExtClassLoader實現,它負責加載<JAVA_HOME>libext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的全部類庫,開發者能夠直接使用擴展類加載器。

應用程序類加載器(Application ClassLoader):

這個類加載器由sun.misc.Launcher$AppClassLoader實現。因爲這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,因此通常也稱它爲系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者能夠直接使用這個類加載器,若是應用程序中沒有自定義過本身的類加載器,通常狀況下這個就是程序中默認的類加載器。

咱們的應用程序都是由這3種類加載器互相配合進行加載的,若是有必要,還能夠加入本身定義的類加載器。

4.3 雙親委派模型

雙親委派模型(Pattern Delegation Model),要求除了頂層的啓動類加載器外,其他的類加載器都應該有本身的父類加載器。這裏父子關係一般是子類經過組合關係而不是繼承關係來複用父加載器的代碼。

雙親委派模型(Pattern Delegation Model)
雙親委派模型的工做過程: 若是一個類加載器收到了類加載的請求,先把這個請求委派給父類加載器去完成(因此全部的加載請求最終都應該傳送到頂層的啓動類加載器中),只有當父加載器反饋本身沒法完成加載請求時,子加載器纔會嘗試本身去加載。

使用雙親委派模型來組織類加載器之間的關係,有一個顯而易見的好處就是java類隨着它的類加載器一塊兒具有了一種帶有優先級的層次關係

注意:雙親委派模型是Java設計者們推薦給開發者們的一種類加載器實現方式,並非一個強制性 的約束模型。在java的世界中大部分的類加載器都遵循這個模型,但也有例外。

4.4 破壞雙親委派模型

雙親委派模型主要出現過3次較大規模「被破壞」的狀況。

第一次破壞是由於類加載器和抽象類java.lang.ClassLoader在JDK1.0就存在的,而雙親委派模型在JDK1.2以後才被引入,爲了兼容已經存在的用戶自定義類加載器,引入雙親委派模型時作了必定的妥協:在java.lang.ClassLoader中引入了一個findClass()方法,在此以前,用戶去繼承java.lang.Classloader的惟一目的就是重寫loadClass()方法。JDK1.2以後不提倡用戶去覆蓋loadClass()方法,而是把本身的類加載邏輯寫到findClass()方法中,若是loadClass()方法中若是父類加載失敗,則會調用本身的findClass()方法來完成加載,這樣就能夠保證新寫出來的類加載器是符合雙親委派模型規則的。

第二次破壞是由於模型自身的缺陷,現實中存在這樣的場景:基礎的類加載器須要求調用用戶的代碼,而基礎的類加載器可能不認識用戶的代碼。爲此,Java設計團隊引入的設計時「線程上下文類加載器(Thread Context ClassLoader)」。這樣能夠經過父類加載器請求子類加載器去完成類加載動做。已經違背了雙親委派模型的通常性原則。

第三次破壞 是因爲用戶對程序動態性的追求致使的。這裏所說的動態性是指:「代碼熱替換」、「模塊熱部署」等等比較熱門的詞。說白了就是但願應用程序可以像咱們的計算機外設同樣,接上鼠標、U盤不用重啓機器就能當即使用。OSGi是當前業界「事實上」的Java模塊化標準,OSGi實現模塊化熱部署的關鍵是它自定義的類加載器機制的實現。每個程序模塊(OSGi中稱爲Bundle)都有一個本身的類加載器,當須要更換一個Bundle時,就把Bundle連同類加載器一塊兒換掉以實現代碼的熱替換。在OSGi環境下,類加載器再也不是雙親委派模型中的樹狀結構,而是進一步發展爲更加複雜的網狀結構。

總結:

本節主要介紹了類加載過程當中:「加載」、「驗證」、「準備」、「解析」、「初始化」這5個階段中虛擬機進行了了那些動做,還介紹了類加載器的工做原理及對虛擬機的意義。

歡迎關注個人微信公衆號:"Java面試通關手冊"(一個有溫度的微信公衆號,期待與你共同進步~~~堅持原創,分享美文,分享各類Java學習資源):
微信公衆號

引用文字
相關文章
相關標籤/搜索