1. 程式人生 > >實戰Java虛擬機器筆記 9-10

實戰Java虛擬機器筆記 9-10

class檔案的結構並不是一成不變的,隨著Java虛擬機器的不斷髮展,總是不可避免地會對class檔案結構做出一些調整,但是其基本的結構和框架是非常穩定的。

class檔案的結構: 魔數、小版本號、大版本號、常量池、訪問標記、 當前類、 父類、實現的介面、類的欄位、類的方法、類的屬性。

ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count - 1]; u2 access_flags; u2 this_class; u2 super_class; u2 interface_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_info]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; }

魔數:0xCAFEBABE 在常量池數量之後,就是常量池的實際內容,每一項以型別、長度、內容或者型別、內容的格式依次存放。 Utf8    1 Integer 3 Float    4 Long    5 Double    6 Class    7 String    8 Fieldref 9 Methodref 10 NameAndType 12 作為常量池底層的資料型別CONSTANT_Utf8, CONSTANT_Integer, CONSTANT_Float, CONSTANT_Long, CONSTANT_Double分別表示UTF8字串、整數、浮點數、長整數和雙精度浮點數。 UTF8的常量經常被其他型別的常量引用。 當使用final定義一個數字常量時,class檔案中就會生成一個數字的常量。

CONSTANT_NameAndType的descriptor_index指向的字串使用了一組特定的字串來表示型別: B(byte) S(short) I(int) J(long) F(float) D(double) C(char) Z(boolean) V(void) [(陣列) L;(物件) 如:"Ljava/lang/Object;"表示類java.lang.Object。 "[[Ljava/lang/String;"表示String二維陣列。

對於類的方法和欄位,分別使用CONSTANT_Methodref和CONSTANT_Fieldref表示。兩者結構一樣,tag的值不同。 CONSTANT_Methodref_info {   u1 tag;   u2 class_index;   u2 name_and_type_index; }

在常量池後,緊跟著訪問標記。該標記使用兩個位元組表示,用於表明該類的訪問資訊,如public、final、abstract等。

當前類、父類和介面 u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; 其中,this_class、super_class指向常量池中一個CONSTANT_Class,interfaces中的每一項也指向常量池中的CONSTANT_Class(這裡就必須是介面,而不是類)。

Class檔案的欄位: u2 fields_count; field_info fields[fields_count]; 其中, field_info {   u2 access_flags;   u2 name_index;   u2 descriptor_index;   u2 attributes_count;   attribute_info attributes[attributes_count]; } 一個欄位可能擁有一些屬性,如初始值。這些資訊儲存在attributes陣列中。如: ConstantValue_attribute {   u2 attribute_name_index;   u4 attribute_length;   u2 constantvalue_index; }

Class檔案的方法: 方法資訊和欄位資訊類似 u2 methods_count; method_info methods[methods_count]; 其中, method_info {   u2 access_flags;   u2 name_index;   u2 descriptor_index;   u2 attributes_count;   attribute_info attributes[attributes_count]; } descriptor_index指向的字串有特定格式。如果方法為: Object m(int i, double d, Thread t){...},則方法描述為: (IDLjava/lang/Thread;)Ljava/lang/Object; 和欄位類似,方法也可以附帶若干個屬性,用於描述一些額外資訊,比如方法位元組碼等。 attribute_info {   u2 attribute_name_index;   u4 attribute_length;   u1 info[attribute_length]; }

系統裝載Class型別可以分為載入、連線和初始化3個步驟。 Class只有在必須要使用的時候才會被載入。一個類或介面在初次使用前,必須要進行初始化。這裡指的“使用”,是指主動使用,主動使用只有下列幾種情況: 1. 當建立一個類的例項時,比如使用new關鍵字,或者通過反射、克隆、反序列化。 2. 當呼叫類的靜態方法時,即當使用了位元組碼invokestatic指令。 3. 當使用類或介面的靜態欄位時(final常量除外),比如,使用getstatic或者putstatic指令。 4. 當使用java.lang.reflect包中的方法反射類的方法時。 5. 當初始化子類時,要求先初始化父類。 6. 作為啟動虛擬機器含有main方法的那個類。

類的初始化是類裝載的最後一個階段。如果前面的步驟都沒有問題,那麼表示類可以順利裝載到系統中。此時,類才會開始執行Java位元組碼。初始化階段的重要工作是執行類的初始化方法<clinit>。方法<clinit>是由編譯器自動生成的,它是由類靜態成員的賦值語句以及static語句塊合併產生的。 Java編譯器並不會為所有的類都產生<clinit>初始化函式。如果一個類既沒有賦值語句,也沒有static語句塊,那麼生成的<clinit>函式就應該為空,因此,編譯器就不會為該類插入<clinit>函式。

ClassLoader主要工作在Class裝載的載入階段,其主要作用是從系統外部獲得Class二進位制資料流。因此,ClassLoader在整個裝載階段,只能影響到類的載入,而無法通過ClassLoader去改變類的連線和初始化行為。

在標準的Java程式中,Java虛擬機器會建立3類ClassLoader為整個應用程式服務。它們分別是BootStrap ClassLoader(啟動類載入器)、Extension ClassLoader(擴充套件類載入器)和App ClassLoader(應用類載入器,也稱為系統類載入器)。 在這些ClassLoader中,啟動類載入器最為特別,它是完全由C程式碼實現的,並且在Java中沒有物件與之對應。系統的核心類就是由啟動類載入器進行載入的,它也是虛擬機器的核心元件。擴充套件類載入器和應用類載入器都有對應的Java物件可供使用。

無法在Java程式碼中訪問啟動類載入器,當試圖獲得一個類的ClassLoader時,如果得到的是null,這並不意味著沒有載入器為它服務,而是指載入那個類的為啟動類載入器。 啟動類載入器負責載入系統的核心類,比如rt.jar中的Java類。 擴充套件類載入器負責載入%JAVA_HOME%/lib/ext/*.jar中的Java類。 應用類載入器負責載入使用者類,也就是使用者程式的類。

系統中的ClassLoader在協同工作時,預設會使用雙親委託模式。即在類載入的時候,系統會判斷當前類是否已經被載入,如果已經被載入,就會直接返回可用的類,否則就會嘗試載入,在嘗試載入時,會先請求雙親處理,如果雙親請求失敗,則會自己載入。 判斷類是否載入時,應用類載入器會順著雙親路徑往上判斷,直到啟動類載入器。但是類載入器不會往下詢問,這個委託路線是單向的。

通常情況下,啟動類載入器中的類為系統核心類,包括一些重要的系統介面,而在應用類載入器中,為應用類。按照這種模式,應用類訪問系統類自然是沒有問題,但是系統類訪問應用類就會出現問題。比如,在系統類中,提供了一個介面,該介面需要在應用中得以實現,該介面還繫結一個工廠方法,用於建立該介面的例項,而介面和工廠方法都在啟動類載入器中。這時,就會出現該工廠方法無法建立由應用類載入器載入的應用例項的問題。擁有這種問題的元件有很多,比如JDBC、Xml Parse等。

在Java平臺中,把核心類(rt.jar)中提供外部服務,可由應用層自行實現的介面,通常可以稱為Service Provider Interface,即SPI。 在啟動類載入器載入的類中,可以通過Thread.currentThread().getContextClassLoader()獲取應用類載入器。因為在預設情況下,上下文載入器就是應用類載入器。