天天看點

jvm學習--類加載器

    java程式的從源代碼到執行的過程包括編譯和運作兩個階段。編譯階段由編譯器執行,将源代碼(.java)檔案編譯成位元組碼檔案(class檔案);運作階段由JVM執行,将位元組碼檔案加載到記憶體中,變為虛拟機可以直接使用的資料結構,該過程即為類加載機制。

類加載過程包括如下7個階段:

1)加載:從位元組碼二進制變為Class對象;

2)驗證:校驗位元組碼格式是否合法;

3)準備:為類變量static修飾變量賦初始零值,配置設定記憶體;

4)解析:将常量池中的符号引用替換為直接引用;

5)初始化:執行類構造器,包括:給類變量賦預設值,執行類中的靜态代碼塊;

6)使用:在程式方法中使用類;

7)解除安裝:對方法區(元空間)中的Class對象進行GC回收,清除不必要的Class對象;

    其中驗證、準備、解析3個階段被合稱為連接配接階段,即将Class對象與記憶體關聯映射的過程。為了保證類加載的靈活性,java虛拟機規範僅要求加載、驗證、準備、初始化、解除安裝的順序固定,對于解析在什麼階段進行并沒有給出詳細限制,解析階段也可以發生在初始化之後,用于支援運作時綁定(晚綁定、動态綁定)。

注意:此處的生命周期都是針對單個類而言的,出于性能考慮,jvm施行按需加載的政策,隻有當類将要被使用時,才會加載。并不會在jvm啟動時就加載所有的類。是以類加載的完整過程可能發生在jvm運作的任何時候。

    加載階段是整個加載過程的一部分,是指将class二進制流轉換為Class對象,存入記憶體模型中的方法區(元空間)的過程;加載分為2類:普通類的加載和數組類的加載。

    普通類的加載是指直接通過類加載加載的類。與之相對應的數組類的加載不是由類加載器加載的。在java中,數組變量也是一種對象,因而具有對象的類型。數組對應的類型,是由虛拟機在運作時自動建立并加載的,其類的全限定名是在數組元素類型的全限定名之前加上[L,比如mypackage.MyClass對應的數組類型為[Lmypackage.MyClass。

1) 通過類的全限定名(包名.類名)擷取類的二進制位元組流。之是以限定為二進制流,而非檔案,是為了提高靈活性,Java在類的資料,既可以來自本地檔案,也可以來自網絡資料流,甚至可以通過位元組碼生成工具自動生成。這就極大地豐富了”創造”類對象的手段,比如: jdk提供的動态代理技術在Proxy中,通過ProxyGenerator.generateProxyClass來為特定的接口生成形式為*$Proxy的代理類二進制位元組流,為AOP的實作提供了基礎。

2) 将位元組流所代表的靜态存儲結構轉化為方法區(jdk1.8為元空間)的運作時資料結構。

3) 在記憶體中生成一個代表這個類的java.lang.Class對象,作為方法區(jdk1.8為元空間)這個類資料的通路入口;

問:元空間内類資料存放的結構是怎樣的,是否有規範可循?

1) 如果數組的元素類型是引用類型,則遞歸加載元素類型,然後在元素類型所屬的類加載器的類名空間中辨別數組類;即:數組類型和元素類型使用相同的類加載器加載;

2) 如果數組的元素類型是基礎類型,則在系統類加載器(AppClassLoader)的類名空間中辨別數組類;即:數組類型使用系統類加載器加載;

3) 生成數組類的可見性與元素類型的可見性一緻;如果元素類型是基礎類型,則數組類型的可見性預設為public;

問1:當數組元素類型為基礎類型時,基礎類型是由引導類加載器(BootstrapClassLoader)加載的,是否是因為引導類加載器對于數組類型而言不可見,故使用系統類加載器? 問2:類和類加載器是如何關聯的?

    此處要區分類的加載和初始化2個階段,當出現如下代碼時,雖然不會觸發類的初始化,但會觸發類的加載;

運作結果如下,可以看到NotLoad類和ElementClass類的加載資訊:

    由于靜态塊是在類的初始化階段執行,而結果中并未列印靜态塊中的語句,因而可以斷定jvm位對ElementClass類進行初始化;

    驗證階段的主要目的是為了確定class位元組流資料的合法性,防止損害虛拟機自身的安全。因而驗證階段可以看做是出于安全考慮而增加的額外階段,假定所加載的class位元組流可以保證安全,則該階段可以跳過。通過-Xverify:none參數可以關閉驗證。

1) 檔案格式驗證:驗證位元組流是否符合Class檔案格式規範;

2) 中繼資料驗證:語義分析,對中繼資料的資料類型進行校驗,保證位元組碼描述資訊符合JAVA語言規範;

3) 位元組碼驗證:通過資料流和控制流分析,确定程式的語義是合法的,主要是對方法體中代碼的分析;

4) 符号引用驗證:發生在将符号引用轉化為直接引用時(與解析階段重疊),校驗符号引用是否能夠找到比對的類;

    該優化僅在jdk<1.7時有效。優化原因是位元組碼驗證複雜度高,對性能消耗較多。為了提高運作時位元組碼驗證的效率,将資料流分析提前到編譯階段完成,并将分析結果存放到位元組碼檔案Code屬性表的StackMapTable屬性中。進而在校驗時,直接讀取StackMapTable中的分析結果進行校驗即可,縮短了校驗時間。但該優化也可能存在風險,即StackMapTable是存放在位元組碼檔案中的,本身也是可以被篡改的。可以通過-XX:-UseSplitVerifier參數關閉StackMapTable優化。

    準備階段主要是為類變量(static修飾)配置設定記憶體,賦初始零值。此處的零值并非代碼中顯式為類變量賦予的預設值,而是指資料類型的零值。如果是常量(static final修飾,且字段屬性表存在ContantValue屬性),則初始值為常量值。ContantValue屬性的值是在編譯時放入的。

資料類型的零值,而非代碼中給出的預設值。為static變量賦預設值的操作,是在初始化階段執行類構造器clinit時由putstatic指令完成的。clinit是在編譯階段生成的。不同的資料類型的零值如下:

引用類型零值為null;

數值類型零值為0;

boolean值類型零值為false;

char類型零值為u0000;

    将常量池中的符号引用替換為直接引用。符号引用的解析是原子性的,對于同一符号引用的多次解析,要麼全部成功,要麼全部失敗。

    JVM規範并未規定解析階段發生的具體時間,即虛拟機實作可以根據需要判斷在類加載時就進行解析,還是在一個符号引用将要被使用前才去解析。這樣做的主要目的是為了提高類整個類加載過程的靈活性。

    符号引用相當于一個占位符,用該占位符來描述代碼執行時所引用的目标。目标并不局限于類/接口,它可以是:類/接口、字段、類方法、接口方法、方法類型、方法句柄、調用點限定符。符号引用并不需要考慮實際的記憶體布局,隻要能夠唯一标定要引用的目标即可。

    直接引用是引用目标記憶體位址的辨別,可以是一個指針、相對偏移量或者一個能夠間接定位到目标的句柄。與記憶體布局強相關,因而不同的虛拟機執行個體上相同目标的直接引用一般不同。直接引用代表了引用目标在記憶體中的存在性,如果有直接引用,說明引用目标在記憶體中一定存在。

    解析過程中,可能存在對于同一個符号引用進行多次解析請求。為了提高效率,避免重複解析,可以對符号引用進行緩存(在運作時常量池中記錄直接引用,并把常量辨別為已解析狀态);

    解析都是針對方法體或者代碼塊中的執行的語句來說的。解析的目的是将方法體或者代碼塊中執行語句的符号引用替換為直接引用。對于成員變量直接賦引用或者用new操作符建立的情況,實際是在類構造器和執行個體構造器中執行的;亦可看做是在方法中的語句。

1) 如果引用目标不是數組類型,則根據全限定名加載目标類;加載目标類使用目前類的類加載器;

2) 如果引用目标是數組類型,且數組的元素類型引用類型,則先加載數組的元素類型,再建立數組類型對象;

3) 如果上述完成,則驗證對引用目類标是否具有通路權限;如果不具有通路權限,則抛出java.lang.IllegalAccessError錯誤;

1) 先解析字段所屬類/接口的符号引用,然後解析字段的符号引用;

2) 字段的直接引用查找順序:

jvm學習--類加載器

1) 先解析方法所屬類/接口的符号引用,然後解析方法的符号引用;

2) 類和接口方法符号引用的常量類型定義是分開的,需要分别解析;

3) 類方法的直接引用查找順序:

注:類方法和接口方法引用的查找的差別在于,類方法一定要有一個實作了的方法,否則抛出異常;

4) 接口方法的引用查找順序:

jvm學習--類加載器

    初始化階段是執行類構造器的階段。類構造器是有編譯器生成的,主要用來為類的靜态變量設定預設值,執行靜态代碼塊。

1) clinit方法是由編譯器自動收集類中的所有類變量(靜态成員變量)的指派動作和靜态代碼塊中的語句合并産生的;

2) 編譯器的收集順序是由類變量指派語句和靜态代碼塊在源檔案中出現的順序決定的;

3) 靜态代碼塊隻能通路定義在其前面的類變量,但可以給定義在其後的類變量指派;

4) clinit方法無需在調用自己之前,調用父類的clinit方法,因為虛拟機能夠保證先調用父類的clinit方法,第一個被執行的clinit方法一定是java.lang.Object類;

5) 因為父類的clinit方法先執行,是以父類的靜态代碼塊要先于子類的靜态代碼塊和類變量指派語句執行;

6) 接口和父接口的clinit方法執行順序不需要保證,因為接口中沒有定義靜态塊,隻可能出現接口變量指派的情況;而接口變量指派的情況不需要保證順序;

7) clinit方法并不是必需的,如果類中沒有對類變量的指派語句,也沒有靜态代碼塊,則編譯器不會為類生成clinit方法;

8) 虛拟機會保證一個類clinit方法在多線程環境中被正确的加鎖、同步;如果多個線程同時去初始化一個類,隻有一個線程執行clinit方法,其餘線程會被阻塞,直到方法執行完才被喚醒,且喚醒後不會再次執行clinit方法;

9) 對于同一個類加載器,一個類型隻會初始化一次,是以一個類的clinit方法隻會被執行一次;

    類的初始化分為2中:類的初始化和接口的初始化。類的初始化主要包括:類變量指派語句、靜态代碼塊初始化兩部分。接口的初始化隻包括類變量的指派語句。

所有類初始化觸發條件的先決條件是:類未被初始化;

1) 代碼中遇到new、getstatic/setstatic、invokestatic指令時,執行初始化;4條指令分别對應的操作是建立一個對象,讀取/設定類變量,調用類的靜态方法。

2) 通過java.lang.reflect包的方法對類進行反射調用;

3) 初始化子類時,如果父類未初始化,則先初始化父類;初始化接口時,與此處有差別,接口初始化不要求先初始化接口的所有父接口;

4) 啟動虛拟機時,如果主類(包含main方法的類)未初始化,則先初始化主類;

5) 支援動态語言時,java.lang.invoke.MethodHandle執行個體解析的結果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,句柄對應的類未初始化,則先初始化;

1) 通過子類引用父類的靜态字段,則隻初始化父類,子類不會被初始化;對于靜态字段,隻有直接定義這個字段的類,在引用時會被初始化。比如下面語句不會觸發SubClass類的初始化:

2) 建立數組對象,不會觸發數組元素的初始化;newarray指令:定義某個類型的數組時,不會觸發該類的初始化;隻會觸發數組類的初始化。比如如下代碼,會觸發[Lmypackage.MyClass數組類的初始化;

3) 常量傳播優化:常量在調用時,存儲調用類的常量池中,本質上并沒有直接引用到定義常量的類,故不會觸發定義常量類的初始化;比如如下代碼:

    建立類的數組時,并不進行mypackage.MyClass類的初始化,而是進行[Lmypackage.MyClass的初始化;

    [Lmypackage.MyClass類代表了mypackage.MyClass類的一維數組類型,由newarray指令建立。該類封裝了數組的通路方法,包括:clone()和length()方法。

    數組的建立使用newarray指令而非new指令;當使用newarray指令時,會觸發[Lmypackage.MyClass類的建立,這是由虛拟機自動生成的、直接繼承java.lang.Object的子類。

    [Lmypackage.MyClass類記錄數組的中繼資料和通路方法,為了更好的進行數組類型校驗和安全通路;數組越界檢查封裝在xaload和xastore位元組碼指令中,每次通路或者修改數組都會進行越界檢查;如果通路索引越界,則跑出java.lang.ArrayIndexOutOfBoundsException異常;

對比:c/c++對于數組的通路直接翻譯為數組指針的移動,因而不能進行安全檢查;

為了提高性能,在編譯階段會将java類中被final static修飾的常量直接放到調用類自己的常量池中;調用類對常量的引用實際轉化成了對自己常量池的引用;因而在調用時,不會加載定義常量的類;

    差別:初始化子接口時,不會要求父接口全部初始化,隻有真正用到父接口時才會初始化;但編譯器仍然會為接口生成類構造器(),用于初始化接口中的成員變量。

    原因:類中可以定義static塊,該塊需要在類初始化後執行,且有執行順序的要求,需要先執行父類中的static塊,再執行子類中的static,是以需要先初始化父類;而接口中不允許static塊,是以無需初始化父類。

    虛拟機設計團隊把類加載階段中的"通過一個類的全限定名來擷取描述此類的二進制位元組流"這個動作放到JVM外部去實作,以便讓應用程式自己決定如何去擷取所需要的類。

    實作這個動作的代碼子產品稱為"類加載器"。每一個類加載器都有一個獨立的類名稱空間,是以類的唯一性需要類加載器和類本身一起确定。

    類相等的前提是需要在同一個類加載器的前提下判斷,不同的類加載器加載相同的類,equals()/isAssignableFrom()/isInstance()/instanceof方法結果都會傳回false。

    除了Boostrap的其它類加載器都繼承自抽象類:java.lang.ClassLoader。

    負責加載放在${JAVA_HOME}/lib目錄中的類,或者由-Xbootclasspath參數所指定的路徑中,且是被虛拟機識别的類庫;虛拟機按照名稱識别類。Bootstrap類加載器無法被java程式直接引用,使用者自動義類加載器時,如果需要把加載請求委派給Bootstrap類加載器,則直接傳回null即可。

    負責加載${JAVA_HOME}/lib/ext目錄中的類,或者被java.ext.dirs變量所指定路徑中的類庫。擴充類加載器可以直接使用。類型:sun.misc.Launcher.ExtClassLoader。

    又稱為系統類加載器;負責加載使用者類路徑${CLASSPATH}上的所指定的類庫。通過ClassLoader.getSystemClassLoader()方法可以獲得,開發者可以直接使用。如果使用者沒有自定義類加載器,則預設使用系統類加載器。類型:sun.misc.Launcher.AppClassLoader。

    使用者自定義的類加載器;可通過重寫loadClass方法或者findClass方法實作。

    兩種實作方式的差別在于重寫loadClass可以不遵守雙親委派模型,而重寫findClass仍然遵守雙親委派模型。

    為每個線程提供一個上下文類加載器,調用Thread.setContextClassLoader()方法進行設定。如果目前線程沒有設定,則會從父線程的類加載器。如果應用全局範圍沒有設定,則預設使用系統類加載器。

    便于基礎類庫調用上層服務的類庫。比如,涉及SPI(Service Provider Interface,服務提供商接口)接口調用的場景,虛拟機在加載SPI的類庫時,使用Thread的ContextClassLoader進行加載,具體廠商可以在加載前通過setContextClassLoader方法指定Thread的ContextClassLoader,用來加載自己的業務實作,進而實作了基礎服務調用具體的上層業務實作的功能。

    雙親委派模型是一種職責鍊模式實作,每個類加載器都包含一個parent的類加載器屬性,用于存放上一級類加載器的引用,進而組成一個類加載器的調用鍊。調用鍊從最頂層開始類加載,每個類加載器都隻負責加載符合自身加載條件的類。類加載器按照層次自上而下分别是:Bootstrap類加載器、擴充類加載器、應用程式類加載器、自定義類加載器。其中,Bootstrap類加載器和擴充類加載器分别用來加載JVM運作所需的基本類庫和擴充類庫;應用程式類加載器和自定義類加載器則用來加載程式運作所需的類庫。類加載器中的這種層次關系稱為雙親委派模型。如下圖:

jvm學習--類加載器

    雙親委派模型要求除了頂層的啟動類加載器外,其餘的類加載器都應當有自己的父類加載器。類加載器之間的父子關系一般不會以繼承關系來實作,而是由組合關系來複用父加載器的代碼(合成複用原則)。雙親委派模型并非強制限制,允許根據業務需求更改。

    虛拟機中所有類的加載,會按照自頂向下的優先級執行加載,父類加載器優先加載,如果失敗,再交由子類加載器加載:

1) 類加載器在收到類加載請求時,會先委派給父類進行加載,調用父類的loadClass方法;

2) 如果目前父類加載器沒有父類(父類為null),則使用Bootstrap類加載器進行類加載;如果加載失敗,則抛出ClassNotFound異常;

3) 子類加載器捕獲異常,進行類加載;如果加載失敗,則抛出異常,交由下一級子類加載器;

過程如下:

jvm學習--類加載器

雙親委派模型的邏輯在ClassLoader類的loadClass方法中實作,代碼主要邏輯如下:

    方法首先檢查是否已經加載過。若沒有加載,則調用父類加載器的loadClass()方法進行類加載。若父類加載器為null,則預設使用Bootstrap類加載器作為父類加載器。如果父類加載失敗,則抛出ClassNotFoundException異常,此時再調用目前類加載器的findClass()方法進行加載。

固化類的加載次序,保證類加載層次結構的穩定性,基礎類庫一定是由層次較高的父類加載器進行加載,進而保證了類的唯一性,避免混亂。

1) 涉及SPI(Service Provider Interface,服務提供商接口)接口調用的場景,即基礎子產品調用自定義子產品的情況;比如:JDBC、JNDI等;

2) Servlet容器的實作,要求不同的web應用使用不同的類加載器加載,確定隔離性;

3) OSGI實作,為了實作子產品的熱替換,每個子產品(Bundle)包含一個自己的類加載器,當需要替換子產品時,将子產品連同類加載器一同替換;

職責鍊模式

1) 《深入了解java虛拟機(第2版)》第7章 虛拟機類加載機制;