Java虛擬機之連接模型

       Java程序在運行之前,每個類和接口都是獨立的class文件。JVM是怎樣裝載和解析這些class文件,使它們之間能夠相互關聯呢?下面我們來深入研究Java體系結構中非常重要的一方面——連接模型。
       Java程序經過編譯後,得到的是每個類或者接口的獨立的class文件。雖然這些文件看上去毫無關聯,但是JVM通過動態連接過程,使它們之間通過接口(harbor)符號相互聯繫,或與Java API的class文件聯繫。
       Class文件把它所有的引用符號保存在一個地方——常量池。每一個class文件有一個常量池,每一個被Java虛擬機裝載的類或者接口都有一份內部版本的常量池,被稱作運行時常量池。運行時常量池是一個特定於實現的數據結構,數據結構映射到class文件中的常量池。因此當一個類型被首次裝載時,所有來自於類型的符號引用都裝載到了類型的運行時常量池。
       當程序運行到某個時刻,如果某個特定的符號引用將要被使用,它首先要被解析。解析過程就是根據符號引用查找到實體,再把符號引用替換成一個直接引用的過程。因爲所有的符號引用都保存在常量池中,所以這個過程常被稱作常量池解析。來自相同或不同方法中的幾條指令,可能指向同一個常量池入口,但是每一個常量池入口都只被解析一次。當符號引用被一條指令解析過後,來自其他指令的訪問該符號引用,都使用第一次解析出的直接引用結果。

       不同的Java虛擬機實現允許在程序執行的不同時間進行解析,主要有兩種方式:
       早解析:從初始類開始,到後續的各個類,直到所有的符號引用都被解析
       遲解析:只會在執行程序第一次用到這個符號引用的時候纔去解析

1.動態擴展

       動態擴展:程序運行過程中,通過傳遞類型的名字到java.lang.Class的forName()方法,或者用戶自定義的類裝載器的loadClass()方法,臨時決定裝載和使用的類型。
       動態擴展的兩種方式:

  • 使用java.lang.Class的forName()方法

      public static Class<?> forName(String className)
                        throws ClassNotFoundException
      public static Class<?> forName(String name,
                               boolean initialize,
                               ClassLoader loader)
                        throws ClassNotFoundException
       String類型的className參數表示裝載類型的全限定名;boolean類型的initialize參數表示是否在forName()方法返回前連接並初始化;ClassLoader類型的loader參數表示用戶定製的類裝載器的引用,如果用默認的啓動類裝載器,只需傳遞null。

  • 使用用戶自定義類裝載器的loadClass()方法

      public Class<?> loadClass(String name)
                   throws ClassNotFoundException
      protected Class<?> loadClass(String name,
                             boolean resolve)
                      throws ClassNotFoundException
      String類型的name參數表示裝載類型的全限定名;boolean類型的resolve參數表示是否在裝載時執行該類型的連接。

2.常量池解析

1. 解析CONSTANT_Class_info入口

       這種入口類型用來表示指向類(包括數組類)和接口的符號引用。

  • 數組類

       指向數組類的符號引用最終被解析爲一個Class實例。
       如果數組的元素類型是引用類型,虛擬機用當前類裝載器解析元素類型。如果數組的元素類型是基本類型,那麼虛擬機立即創建關於那個元素類型的新數組類,維數也在此時確定,然後創建一個Class的實例來代表這個類型。如果是關於引用的數組,數組會標記爲是由定義它的元素類型的類裝載器定義的。如果是關於基本類型的數組,數組類會被標記爲是由啓動類裝載器定義的。

  • 非數組類和接口

      要解析任何指向非數組類和接口的符號引用,需要執行如下步驟:

步驟1:裝載類型

      步驟1a:裝載類型或者任何超類型
      虛擬機必須確定是否被引用的類型已經被裝載進了當前命名空間。對於每一個類裝載器,Java虛擬機維護一張列表,其中記錄了所有其裝載的類型的名字。每張這樣的列表,就是JVM內部的命名空間,能夠每個類裝載器都只裝載一次給定名字的類型。
      如果引用的類型是一個類,並且不是java.lang.Object,JVM會裝載它的超類,一直重複到超類爲Object爲止。在從Object返回的路上,JVM裝載每個類型直接實現的任何接口。在裝載接口的時候,JVM會裝載它們直接擴展的任何其他接口。這樣就可以確保符號引用的類型以及該類型的超類和超接口都被裝載了。
     步驟1b:訪問權限檢查
     如果發起引用的類型沒有訪問被引用的類型的權限,JVM拋出IllegalAccessError異常。

步驟2:連接並初始化類型和任何超類

     JVM解析某類型(不是接口)符號引用之前,必須確認它的所有超類都已經被初始化。超類必須在子類之前被初始化。如果一個類型還沒有被連接,在初始化之前必須被連接。只有超類必須被初始化,超接口是不必的。
     步驟2a:校驗類型
     校驗階段可能要求虛擬機裝載新的類型來確認字節碼符合java語言的語義。其他類可能被裝載,甚至被連接,但是肯定不會被初始化。如果在校驗階段JVM出現問題,會拋出VerifyError異常。
     步驟2b:準備類型
     JVM爲類變量以及隨實現不同而有差別的數據結構分配內存。
      步驟2c:可選的步驟,解析類型
      關於被引用的類型,步驟1a、2a、2b已經解析了發起引用的類型的常量池的CONSANT_Class_info入口。步驟2c是關於被引用類型(而非發起引用的類型)中所包含的符號引用的解析。
      步驟2d:初始化類型
      初始化包括兩個步驟:如果類型擁有任何超類,初始化類型的超類是按照自頂向下的順序進行的;如果類型擁有一個類初始化方法,那也在此時執行。

2. 解析CONSTANT_Fieldref_info入口

       要解析類型是CONSTANT_Fieldref_info的常量池入口,虛擬機必須首先解析class_index項中指明的CONSTANT_Class_info入口。如果解析CONSTANT_Class_info成功完成,JVM按照如下步驟執行字段搜索過程:
      1)虛擬機在被引用的類型中查找具有指定的名字和類型的字段。如果虛擬機找到了這樣一個字段,這個字段就是成功的字段搜索結果。
       2)否則,虛擬機檢查類型直接實現或擴展的接口,以及遞歸地檢查它們的接口。如果找到名字和類型都符合的字段,這個字段就是成功的字段搜索結果。
       3)否則,如果類型擁有一個直接的超類,虛擬機檢查類型的直接超類,並且遞歸地檢查類型的所有超類。如果找到了名字和類型都符合的字段,這個字段就是成功的字段搜索結果。
       4)否則,字段搜索失敗。
       如果字段搜索失敗,JVM拋出NoSuchFieldError異常;如果字段搜索成功,但是當前類沒有權限去訪問該字段,JVM拋出IllegalAccessError異常。如果字段搜索到並且有訪問權限,JVM把這個入口標記爲已解析,並在這個常量池入口的數據中放上指向這個字段的直接引用。

3. 解析CONSTANT_Methodref_info入口

      要解析類型是CONSTANT_Methodref_info的常量池入口,虛擬機必須首先解析class_index項中指明的CONSTANT_Class_info入口。如果解析CONSTANT_Class_info成功完成,JVM使用如下步驟執行方法解析:
      1)如果被解析的類型是一個接口,而非類,JVM拋出IncompatibleClassChangeError異常
      2)否則,被解析的類型是一個類。JVM檢查被引用的類是否有一個方法符合指定的名字以及描述符。如果JVM找到了這樣的一個方法,這個方法就是成功的方法搜索結果。
      3)否則,如果類有一個直接的超類,JVM檢查類型的直接超類,並且遞歸地檢查類的所有超類,查看是否有方法符合指定的名字以及描述符。如果找到了這樣的一個方法,這個方法就是成功的方法搜索結果。
      4)否則,JVM檢查是否這個類直接實現了任何接口,並且遞歸地檢查由類型直接實現的接口的超接口。查找是否有方法符合指定的名字以及描述符,如果找到了這樣的一個方法,這個方法就是成功的方法搜索結果。
      5)否則,方法搜索失敗。
       如果JVM沒有找到名字、返回類型、參數數量和類型都符合的方法,JVM拋出NoSuchMethodError異常。如果方法存在,但是方法是一個抽象方法,JVM拋出AbstractMethodError異常。如果方法存在,但是當前類沒有訪問權限,JVM拋出IllegalAccessError異常。否則,JVM將這個常量池入口標記爲已解析,並在數據中放上指向該方法的直接引用。

4. 解析CONSTANT_InterfaceMethodref_info入口

       要解析類型是CONSTANT_InterfaceMethodref_info的常量池入口,虛擬機必須首先解析class_index項中指明的CONSTANT_Class_info入口。如果解析CONSTANT_Class_info成功完成,JVM按照如下步驟來執行接口方法解析:
     1)如果被解析的類型是一個類,而非接口,JVM拋出IncompatibleClassChangeError異常。
     2)否則,被解析的類型是一個接口。JVM檢查被引用的接口是否有方法符合指定的名字和描述符。如果發現了這樣的一個方法,該方法就是成功的接口方法搜索結果。
     3)否則,JVM檢查接口的直接超接口,並且遞歸的檢查接口的所有超接口以及java.lang.Object類來查找符合指定名字和描述符的方法。如果發現了這樣的一個方法,該方法就是成功的接口方法搜索結果。
     4)如果JVM沒有在被引用的接口和它的任何超類中找到名字、返回類型、參數的數量和類型都符合的方法,JVM拋出NoSuchMethodError異常。
     5)否則,JVM將這個常量池入口標記爲已解析,並在數據中放上指向該方法的直接引用。

5. 解析CONSTANT_String_info入口

      要解析類型是CONSTANT_String_info的入口,JVM必須把一個指向字符串對象的引用放置到要被解析的常量池入口數據中。該字符串對象(java.lang.String類的實例)必須按照string_index項在CONSTANT_String_info中指明的CONSTANT_Utf8_info入口所指定的字符順序組織。
      每一個JVM必須維護一張內部列表,它列出了所有在運行程序的過程中已被“拘留(intern)”的字符串對象的引用。維護這個列表的關鍵是任何特定的字符序列在這個列表上只出現一次。在Java程序中,可以調用String類的intern()方法來拘留一個字符串。
      要拘留CONSTANT_String_info入口所代表的字符序列,JVM要檢查內部拘留名單上這個字符序列是否已經在編了。如果已經在編,JVM使用指向以前拘留的字符串對象的引用。否則JVM按照這個字符序列創建一個新的字符對象,並把這個對象的引用編入列表。

6. 解析其他類型的入口
       CONSTANT_Integer_info、CONSTANT_Long _info、CONSTANT_Float _info和CONSTANT_Double _info入口本身包含它們所表示的常量值,它們可以直接被解析。 CONSTANT_Utf8_info和CONSTANT_NameAndType_info類型的入口永遠不會被指令直接引用。它們只有通過其他入口類型才能被引用,並且在那些引用入口被解析時才被解析。

發佈了34 篇原創文章 · 獲贊 22 · 訪問量 38萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章