java虛擬機學習筆記——連接模型(第八章)

8.1 動態連接和解析

Class文件把它所有的引用符號保存在一個地方——常量池。每一個class文件有一個常量池,每一個被Java虛擬機裝載的類或者接口都有一份內部版本的常量池,被稱作運行時常量池。運行時常量池是一個特定於實現的數據結構,數據結構映射到class文件中的常量池。因此當一個類型被首次裝載時,所有來自於類型的符號引用都裝載到了類型的運行時常量池。

當程序運行到某個時刻,如果某個特定的符號引用將要被使用,它首先要被解析。解析過程就是根據符號引用查找到實體,再把符號引用替換成一個直接引用的過程。因爲所有的符號引用都保存在常量池中,所以這個過程常被稱作常量池解析。

記住:java虛擬機爲每一個裝載的類和接口保存一份獨立的常量池。

8.1.1解析和動態擴展

Java的體系結構允許動態擴展Java程序,這個過程包括運行時決定所使用的類型,裝載它們,使用它們。通過傳遞類型的名字到java.lang.Class的forName()方法,或者用戶自定義的類裝載器的loadClass()方法,可以動態擴展Java程序。動態擴展的兩種方式:

1)  直接使用java.lang.Class的forName()方法

Public static Class forName(String className)throwsClassNotFoundException;

//該方法使用當前類的類裝載器,並且總是初始化該類型

Public satic Class forName(String className,Booleaninitialize,ClassLoader loader)throws ClassNotFoundException;

//initialize參數可以指定是否在裝載完後進行初始化,loader可以指定裝載的類裝載器。

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

如果需要用自定義的類裝載器請求類型,只需要調用那個類裝載器的loadClass()方法。

Protected Class loadClass(String name)throws ClassNotFoundException

Protected Class loadClass(String name,Boolean resolve)throwsClassNotFoundException;

兩個loadClass()方法都接受裝載類型的全限定名裝入String類型的name參數。雙參數版本的loadClass()中,boolean類型的參數表示是否在裝載時執行該類型的連接。

區別:如果沒有特別要使用類裝載器的要求,應該用forName(),如果需要請求的類型在裝載時就初始化的話,則不得不使用forName();如果需要一些特定的裝載類型的方法,比如從網絡上下載,從數據庫中取出,從加密文件中提取,甚至動態地創建它們,這時就需要一個類裝載器。

8.1.4 解析CONSTANT_Class_info入口

常量池入口類型中,解析起來最複雜的就是CONSTANT_Class_info了。

數組類的解析:

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

非數組類和接口的解析:

由於需要分多步來解析,下面以1a到2d來說明這一過程。

步驟1:作爲解析的第一步,虛擬機必須確定是否被引用的類型已經被裝載進了當前命名空間,如果沒有被裝載進當前命名空間,虛擬機把類型的全限定名傳遞給當前類裝載器。

步驟1a:裝載類型或者任何超類型

        對於每一個類裝載器,Java虛擬機維護一張列表,其中記錄了所有其裝載的類型的名字。每一張這樣的列表就組成了java虛擬機內部的命名空間。虛擬機會使用雙親委派模型來裝載類型,一旦被引用的類型被裝載了,虛擬機仔細檢查它的二進制數據。如果類型是一個類,並且不是java.lang.Object,虛擬機根據類的數據得到它的直接超類的全限定名。虛擬機接着會察看超類是否已經被裝載進當前命名空間了。如果沒有,先裝載超類。一旦超類被裝載了,虛擬機再次檢查它的二進制數據來找到它的超類。一直重複到超類爲Object爲止。然後在從Object返回的路上,虛擬機再次檢查每個類型是否直接實現了任何接口,如果這樣,它會確保那些接口也被裝載了。經過步驟1a,java虛擬機確認某個類型是否被裝載了,並確保它的所有超類和所有超接口都被裝載了。

步驟1b:檢查訪問權限

        如果發起引用的類型沒有訪問被引用的類型的權限,虛擬機拋出IllegalAccessError異常。檢查訪問權限是在正式校驗階段之前進行的。

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

步驟2a:校驗類型

        這一步就是第七章中的正式校驗階段,校驗階段可能要求虛擬機裝載新的類型來確認字節碼符合java語言的語義。

步驟2b:準備類型

        在準備階段虛擬機爲類型變量以及隨實現不同而有差別的數據結構(如方法表)分配內存。

步驟2c:可選的步驟,解析類型

        步驟1a、2a、2b已經解析了發起引用的類型的常量池的CONSANT_Class_info入口。步驟2c是關於被引用類型中所包含的符號引用的解析。

步驟2d:初始化類型

        如果類型擁有超類,初始化類型的超類是按自頂向下的順序進行的。

8.1.5解析CONSTANT_Fieldref_info入口

    要解析類型是CONSTANT_Fieldref_info的常量池入口,虛擬機必須首先解析class_index項中指明的CONSTANT_Class_info入口。如果CONSTANT_Class_info解析成功,虛擬機按照如下步驟執行字段搜索過程:

1)  虛擬機在被引用的類型中查找具有指定的名字和類型的字段。如果虛擬機找到了這樣一個字段,這個字段就是成功的字段搜索結果。

2)  否則,虛擬機檢查類型直接實現或擴展的接口,以及遞歸地檢查它們的接口。如果找到名字和類型都符合的字段,這個字段就是成功的字段搜索結果。

3)  否則,如果類型擁有一個直接的超類,虛擬機檢查類型的直接超類,並且遞歸地檢查類型的所有超類,如果找到了名字和類型都符合的字段,這個字段就是成功的字段搜索結果。

4)  字段搜索失敗。

如果字段搜索到,虛擬機把這個入口標記爲已解析,並在這個常量池入口的數據中放上指向這個字段的直接引用。

8.1.6解析CONSTANT_Methodref_info入口

    要解析類型是CONSTANT_Methodref_info的常量池入口,虛擬機必須首先解析class_index項中指明的CONSTANT_Class_info入口。如果CONSTANT_Class_info解析成功,虛擬機按照如下步驟執行方法解析:

1)  如果被解析的類型是一個接口,而非類,虛擬機拋出IncompatibleClassChangeError

2)  否則,虛擬機檢查被引用的類是否有一個方法符合指定的名字以及描述符。如果虛擬機找到了這樣一個方法,這個方法就是成功的方法搜索結果。

3)  否則,如果類型擁有一個直接的超類,虛擬機檢查類型的直接超類,並且遞歸地檢查類型的所有超類,查找是否有一個方法符合指定的名字以及描述符,如果找到了這樣一個方法,這個方法就是成功的字段搜索結果。

4)  否則,虛擬機檢查類型直接實現或擴展的接口,以及遞歸地檢查它們的接口。查找是否有一個方法符合指定的名字以及描述符,如果找到了這樣一個方法,這個方法就是成功的字段搜索結果。

5)  否則,方法搜索失敗。

如果方法搜索到,虛擬機把這個入口標記爲已解析,並在這個常量池入口的數據中放上指向該方法的直接引用。

8.1.8解析CONSTANT_String_info入口

     每一個java虛擬機必須維護一張內部列表,它列出了所有在運行程序的過程中已被“拘留(intern)”的字符串對象的引用。基本上,如果一個字符串在虛擬機的拘留列表上出現,就說它被拘留了。維護這個列表的關鍵是任何特定的字符序列在這個列表上都只出現一次。

     要拘留CONSTANT_String_info入口所代表的字符序列,虛擬機要檢查內部拘留名單上這個字符序列是否已經在編了。如果已經在編,虛擬機使用指向以前拘留的字符串對象的引用。否則虛擬機按照這個字符序列創建一個新的字符對象,並把這個對象的引用編入列表。

     在Java程序中,可以調用String類的intern()方法來拘留一個字符串。

8.1.11編譯時常量解析

     被初始化爲編譯時常量的靜態final變量的引用,在編譯時被解析爲常量值的一個本地拷貝,這對所有基本類型和java.lang.String都是正確的。

有兩個好處:1)常量值的本地拷貝使得靜態final變量可以用於switch語句中的case表達式。2)條件編譯。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章