深入理解jvm-2Edition-虛擬機器類載入機制

Lqblalala發表於2021-08-15

1、概述-什麼是類載入

  將Class檔案從其他地方(外存、位元組流甚至是網路流中)載入記憶體,

  並對其中資料進行校驗、轉換解析和初始化,最終從其中提取出能夠被虛擬機器使用的Java型別。

  用圖紙造模子,該模子能夠用於生產物件。

  執行時再進行型別的載入、連結和初始化雖然帶來了一些效能上的影響,

  但是也使得Java可以動態擴充套件。這也是反射等特性的支撐。

  類的生命週期:(巨集觀上的,具體可能會相互交叉巢狀)

    1、載入(載入記憶體,真正被虛擬機器看見)

    2、驗證(格式、內容邏輯)

    3、準備

    4、解析

    5、初始化

    6、使用

    7、解除安裝

  2、3、4也被統稱為連結階段。

2、什麼時候要進行類載入?

  虛擬機器規範裡面沒有規定何時載入,只確定了這五種情況要初始化(那就肯定要先載入啦):

    1、遇到new、getstatic、putstatic或invokestatic位元組碼指令時,如果類沒有進行過初始化,則要出發其初始化。

      就是使用new例項化、訪問靜態欄位/方法時。

    2、使用java.lang.reflect包的方法對類進行反射呼叫時。如果沒有初始化,也要觸發初始化。

    3、初始化一個類,但是其父類沒有初始化過,也要先對父類進行初始化。

     (父類初始化一定在子類之前,Object類是最先初始化的)。

    4、虛擬機器啟動時,需指定要執行的主類,虛擬機器會先初始化主類

    5、使用動態語言支援時,如果一個java.lang.invoke.MethodHandle例項

      解析結果是REF_getStatic、REF_putStatic、REF_invokeStatic方法控制程式碼

      並且該控制程式碼對應的類沒有初始化過,那麼就要初始化。  

  以上五種行為稱為對一個類進行主動引用,當且僅當這些情況會出生髮初始化。

  如以下情況不是主動引用(那就是被動引用囉):

    1、子類引用父類的靜態欄位不會初始化子類,但是會初始化父類。

    2、通過陣列定義引用類,不會出發被引用類的初始化

      因為類和引用該類建立的陣列不是一個類虛擬機器會生成一個直接繼承自Object的類來表示陣列。

      建立指令為newarray。該類封裝了對陣列的訪問,而不是向C/C++一樣直接去操縱指標,因此更安全

    3、對類的編譯時常量(static final修飾,並且值在編譯時可以確定的欄位)的訪問不會導致初始化

      因為編譯時常量會直接存入類的常量池中,對它的訪問本質上沒有引用到定義它的類。

  介面的載入過程與類的載入過程有一點不同,介面也有初始化過程,

  但是介面初始化時不要求其父介面都完成了初始化。父介面只有在真正用到時才被初始化。

3、深入類載入過程

  1、載入

    載入是類載入過程的一個階段,在這個階段需要完成三件事:

      1、通過類的全限定名來獲取此類的二進位制流

        沒有說從哪裡獲取,那就大有可為了,

        可以從Jar包、網路、由其他檔案生成(JSP)、資料庫中讀取、甚至執行時生成(動態代理)。

      2、將二進位制流中表示的靜態的儲存結構轉化為方法區中的執行時資料結構

      3、在記憶體中(具體是堆還是方法區由JVM具體實現決定)生成一個代表此類的java.lang.Class物件

        此物件作為方法區中的資料的訪問入口

    但是如果是陣列類呢?陣列類是由JVM直接建立的,

    但是畢竟還是要用到最內層的元素型別(Element Type)的類,所以與類載入器由密切關係。

    陣列的建立過程

      1、如果該陣列類的元件型別(Component Type,指該陣列去掉一個維度的型別)

        是引用型別,那就遞迴的去載入這個元件型別

        該陣列類會和載入它的元件型別的類載入器關聯(類的唯一性由它本身和它的類載入器一起確定)。

      2、如果元件不是引用型別,JVM會將該陣列類和引導類載入器(Bootstrap ClassLoader)關聯。

      3、陣列類的可見性(訪問許可權)和它的元件型別一致。

        如果元件型別不是引用型別,那麼訪問許可權預設為public。

    載入階段和連結階段是交叉進行的,還有可能載入階段尚未完成,連結階段就已經開始了。

  2、驗證

    確保位元組流中的內容是符合規範的,是JVM安全性的保證之一。

    1、檔案格式驗證

      驗證位元組流符合Class檔案規範

      包括:魔數、版本號、常量池常量型別、索引值的指向等。

    2、後設資料驗證

      對位元組碼進行語義分析,保證其資訊符合Java語言規範的要求。

      包括:類是否有父類(唯一根類要求)、父類是否允許被繼承(繼承關係的正確性)、

        非抽象類是否實現了其父類或介面中要求實現的所有方法(abstract方法)、類的欄位是否衝突等。

    3、位元組碼驗證

      通過資料流和控制流分析,對類的方法體進行校驗分析,確保被驗證類的方法沒有安全隱患。

      JDK1.6後加入了StackMapTable屬性,描述了方法體中所有基本塊(Basic Block,按照控制流拆分的程式碼塊)

      開始時本地變數表和運算元棧應有的狀態,用於輔助驗證。

      包括:任意時刻運算元棧的資料型別是否和位元組碼指令匹配,跳轉指令的跳轉位置是否恰當, 型別轉換是否有效等。

    4、符號引用驗證

      發生在JVM將符號引用轉換為直接引用的時候,在解析階段中發生。對類自身以外的資訊進行匹配項校驗。

      包括:符號引用中全限定名是否能找到指定類、

        類中描述符和簡單名描述的方法和欄位是否存在、符號引用中的類、方法、欄位是否能被訪問等。

  3、準備

    正式為類變數(static)分配記憶體及設定初始值。類變數使用的記憶體在方法區中分配

    類變數的初始值就是把記憶體區域置零,除非類變數為編譯時常量。

    編譯時常量會在欄位屬性表中有ConstantValue屬性記錄它在編譯期確定的值,此時它能被直接初始化為該值。

  4、解析

    將常量池中的符號引用替換為直接引用的過程。

    符號引用Symbolic References:用符號來描述所引用的目標,

                   與虛擬機器的記憶體佈局無關,引用目標並不一定已經載入到記憶體中。

    直接引用Direct Reference:直接指向目標所在地址的指標、相對偏移量或間接定位的控制程式碼。

                 和虛擬機器記憶體佈局相關

    沒有規定解析階段發生的具體時間,但是在執行操縱符號引用的位元組碼指令之前,先要對它們使用的符號引用進行解析

    解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別、方法控制程式碼和呼叫點限定符等7類符號引用進行。

    就是CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、

      CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、

      CONSTANT_MethodHandle_info、CONSTANT_InvokeDynamic_info 這7個常量型別。

    1、類或介面解析  類D 要把符號引用N  解析為對類或介面C的直接引用

      1、如果C不是陣列型別,那麼JVM會將N中C的全限定名傳遞給D的類載入器,讓其去載入C。

        載入過程可能觸發其他類或介面的載入,如C的父類或介面。

      2、如果C是陣列型別,並且C的元素型別為物件,也就是N類似於"[Ljava/lang/String"的形式,

        那麼會以(1)的方式來載入元素型別,如"java.lang.String"。

        接著JVM生成一個C型別和維度的陣列物件。

      3、上面兩部無異常,則會進行最後一步:符號引用驗證,驗證C能被D訪問(訪問許可權)。

        不滿足則丟擲java.lang.IllegalAccessError異常。

    2、欄位解析

      首先會對欄位表內class_index項中的索引CONSTANT_Class_info進行解析,確定欄位所屬的類

      用C表示欄位的類:

        1、如果C中存在簡單名和欄位描述符都與目標欄位匹配的欄位,那麼,返回該欄位的直接引用,查詢結束。

        2、否則,到C的介面樹上找。找到(簡單名、欄位描述符)則返回。

        3、否則,到C的繼承鏈上找。找到(簡單名、欄位描述符)則返回。

        4、否則,找不到啦,直接報錯!java.lang.NoSuchFieldError

      同樣,最後也要驗證訪問許可權。不滿足則丟擲java.lang.IllegalAccessError異常。

    3、類方法解析

      首先確定方法所屬的類,即對方法表內class_index項中的索引CONSTANT_Class_info進行解析。

      C表示方法所屬類:

        1、類方法和介面方法的符號引用的常量型別是分開的,

          分別是CONSTANT_Methodref_infoCONSTANT_InterfaceMethodref_info

          如果發現方法表內class_index項中的索引CONSTANT_Class_info指向的是一個介面

          則丟擲java.lang.IncompatibleClassChangeError。   

        2、否則,如果C中存在簡單名和方法描述符都與目標方法匹配的欄位,

          那麼,返回該方法的直接引用,查詢結束。

        3、否則,到C的介面樹上找。找到(簡單名、方法描述符)則返回。

        4、否則,到C的繼承鏈上找。找到(簡單名、方法描述符)則返回。

        5、否則,找不到啦,直接報錯!java.lang.NoSuchMethodError

      同樣,最後也要驗證訪問許可權。不滿足則丟擲java.lang.IllegalAccessError異常。

    4、介面方法解析      

      首先確定方法所屬的介面,即對方法表class_index項中的索引CONSTANT_Class_info進行解析。

      C表示方法所屬介面:

        1、類方法和介面方法的符號引用的常量型別是分開的,

          分別是CONSTANT_Methodref_info和CONSTANT_InterfaceMethodref_info

          如果發現方法表內class_index項中的索引CONSTANT_Class_info指向的是一個

          則丟擲java.lang.IncompatibleClassChangeError。   

        2、否則,如果C中存在簡單名和方法描述符都與目標方法匹配的欄位,

          那麼,返回該方法的直接引用,查詢結束。

        3、否則,到C的介面樹上找。找到(簡單名、方法描述符)則返回。

        4、否則,找不到啦,直接報錯!java.lang.NoSuchMethodError

      介面預設都是public的,不存在訪問問題,因此不會丟擲java.lang.IllegalAccessError異常。

  5、初始化

    類載入的最後一步。到了初始化,才真正開始執行Java位元組碼。

    在準備階段,類在分配類變數記憶體時被初始化了一次,那是為了讓類變數的初始值滿足系統要求。

    而初始化階段,是為了讓類變數(static)的初始值滿足程式設計師預先定義的初始值

    初始化可以看作是執行類構造器<clinit>的過程。

    <clinit>方法特點

      1、<clinit>方法是由編譯器自動收集類中所有類變數的賦值動作和靜態語句塊(static{})

        中的語句合併產生的。收集順序按在原始檔中的出現順序決定。

        靜態語句塊中,只能訪問到定義在該靜態語句塊之前的變數,之後的可以賦值,但不能訪問(不能取出值)。

      2、與建構函式不同,<clinit>方法不用顯示呼叫父類構造器,

        因為父類的初始化一定在子類方法之前完成,第一個執行<clinit>方法的類肯定是Object類。

      3、由於父類的<clinit>方法先執行,因此父類的類變數和靜態語句中的語句先被執行。

      4、<clinit>方法不是必須的

        如果沒有類變數的賦值操做,也沒有靜態語句塊,那麼就不會生成<clinit>方法。

      5、虛擬機器要保證<clinit>方法在多執行緒環境下的執行緒安全性

        因此,如果多個執行緒同時初始化一個類,那麼只有一個執行緒會去執行<clinit>方法。

        如果<clinit>方法要耗費很長時間,則可能會造成多執行緒阻塞

4、類載入器

  通過類的全限定名來獲取類的二進位制位元組流是放在虛擬機器之外實現的,程式設計師可以自己決定怎麼去載入。

  1、類和類載入器的關係

    每一個類載入器都有一個獨立的類名稱空間

    任意一個類,都要由它本身和載入它的類載入器一起來確定它在虛擬機器中的唯一性。

    因此,比較兩個類是否相等,要在它們都是同一個類載入器載入的才有意義。

  2、雙親委派模型

    從虛擬機器角度看,只有兩種類載入器:

      1、Bootstrap ClassLoader 啟動類載入器

        JVM一部分,用於JVM啟動時的依賴類載入。

      2、其他類載入器

        不屬於JVM,都繼承自java.lang.ClassLoader

    從開發人員角度:

      1、Bootstrap ClassLoader 啟動類載入器

        載入<JAVA_HOME>\lib目錄下,

        或者被-Xbootclasspath引數指定的目錄下的虛擬機器識別(僅按照檔名識別)的類庫。

        啟動類載入器無法被Java程式直接引用。

      2、Extension ClassLoader 擴充套件類載入器

        載入<JAVA_HOME>\lib\ext目錄下,

        或者被java.ext.dirs系統變數所指定的路徑下的所有類庫。

        開發人員可直接使用。

      3、Application ClassLoader 應用程式類載入器

        也叫系統類載入器,因為它是ClassLoader類getSystemClassLoader() 方法的返回值。

        負責載入使用者類路徑ClassPath上指定的類庫,開發人員可直接使用。

        應用程式如果沒有自定義自己的類載入器,那麼預設就是這個。

    雙親委派模型:

      

 

      當一個類載入器收到載入請求時,它把請求委託給它的父類載入器去完成,

      直到父類無法完成該請求時,它才會嘗試自己去完成。

      【職責鏈設計模式】:事件沿職責鏈往上走,直到遇到能完成它的類。

      這裡也差不多,只是變成了直到遇到不能完成它的類。

    雙親委派有什麼好處?

      因為類的唯一性要由類載入器參與確認,因此如果我們用不同類載入器載入一個Class檔案,那麼會產生不同的類。

      對於java.lang.Object這些底層的類而言,就很要命了。。。

      JVM中出現了很多職責行為一樣,但是卻是不同的類。混亂了!唯一根類也沒辦法滿足了。

      雙親委派模型使得Java類和它的類載入器一起具備了一種帶優先順序的層級關係。

      <JAVA_HOME>\lib下的類都是由Bootstrap ClassLoader載入的,在程式中只會有一份。

      <JAVA_HOME>\lib\ext下的類都是由Extension ClassLoader載入的,也只有一份。

      使用者指定類路徑上的都是由Application ClassLoader載入。

    雙親委派模型實現:

    

  3、破壞雙親委派模型

    歷史上的三次破壞:

      1、雙親委派模型在JDK1.2才引入,之前就有很多程式碼是繼承ClassLoader而沒有實現雙親委派的。

      2、由於模型缺陷

        在一些整合架構中(如JDBC、JNDI),架構的主體是在Java JDK類庫中,

          由Bootstrap ClassLoader來載入。

        但是,架構的具體的模組卻是由獨立廠商實現並部署在應用程式的ClassPath下的。

        由Bootstrap ClassLoader載入的程式碼要呼叫應用程式的ClassPath下的程式碼

        怎麼辦?Bootstrap ClassLoader不能載入這些程式碼啊。。

        因此,引入了Thread Context ClassLoader執行緒上下文類載入器

        Thread Context ClassLoader可由java.lang.Thread類的setContextClassLoader() 方法進行設定

        如果該執行緒沒有設定,那麼它會從父執行緒那裡繼承

        如果全域性都沒有設定,那麼預設值是Application ClassLoader

        現在執行過程就變成了:

          1、架構主體程式碼由Bootstrap ClassLoader來載入。

          當要載入廠商模組時:

          2、先一個方法將執行緒的類載入器設定為自己想要的類載入器,並儲存執行緒之前的類載入器

          3、載入時,用Thread.currentThread.getContextClassLoader()方法取得類載入器。

          4、載入完成,將執行緒的類載入器還原

      3、為了實現熱替換HotSwap

        即插即用,熱部署。OSGi模組化標準

        每一個程式模組(OSGi稱為Bundle)都有一個自己的類載入器,

        當要替換一個Bundle時,連同它的類載入器一起替換。

        OSGi類載入委派模型:

          1、以java.*開頭的類委派給父類載入器

          2、否則,將委派列表名單內的類委派給父類載入器載入。

          3、否則,將import列表中的類委派給export這個類的Bundle的類載入器載入。

          4、否則,查詢當前Bundle的ClassPath,使用自己的類載入器載入。

          5、否則,查詢類是否在自己的Fragment Bundle中,如果在,則委託給Fragment Bundle的類載入器載入。

          6、否則,查詢Dynamic Import列表的Bundle,委派給對應的Bundle的類載入器載入。

        1、2仍然符合雙親委派,其餘都是平級查詢。

      

 

    

 

 

 

 

    

    

相關文章