1. 程式人生 > >Java虛擬機器學習筆記(5)——類檔案結構

Java虛擬機器學習筆記(5)——類檔案結構

          上一篇介紹了JVM物件的記憶體分配和回收策略。這篇接著介紹Java的類檔案結構,這篇的內容可能會比較多,我儘量循序漸進的講。要學習class的檔案結構,先要大體對class檔案結構有哪些內容有一個整體把握。現在,看下面一張表。


          上面的表格列出了class檔案的所有內容項,一定要認真將表格看一遍,對class檔案有大致的結構印象,下面將按照表格總從上到下的順序,一一介紹其中的內容。

          關於上面的表格含義,這裡做一下簡單的解釋,u1、u2、u4、u8代表佔用了1、2、4、8個位元組大小,以info字尾結尾的代表一個結構表。

        在下面對檔案結構的介紹中,為了讓大家對檔案結構有更清晰的瞭解,每一項結構,我都會標明這一項是屬於哪一項的哪個表中的哪個屬性。這樣,當內容多起來時,才不容易混亂。

魔數-magic(屬於class檔案的第一項)

Java 中用魔數標記這是一個Java class檔案。

         參照上面的class檔案表,魔數佔用u4大小。這裡可以明確給出,其實class檔案的魔數是:CA FE BA BE(你可以記成“咖啡寶貝”)。下面我們用16進位制編輯器檢視一下class檔案的頭4位元組。

 

          可以看到,class檔案的頭四個位元組就是CA FE BA BE,它標誌了這個檔案是一個class檔案。

次版本號minor_version與主版本號major_version(屬於class檔案的第二三項)

          這一項沒有什麼好多說的,就是代表class檔案的主次版本號。但需要注意的是,高版本的JDK中的虛擬機器是拒絕執行低版本的class檔案的。


          上圖中,偏移量0x00000004,u2位元組,0x0000代表次版本號,偏移量0x00000006,u2位元組,0x0033代表主版本號(換算成10進製為:51)。

常量池入口constant_pool_count與常量池constant_pool(屬於class檔案的第四五項)

          constant_pool_count,如字面意思,就是常量池計數,大小u2,標記了當前class檔案的常量個數(其實看下去你會發現,所有用來計數的大小都是u2)。

          constant_pool就是常量池了,常量池主要存放兩大類常量:字面量(Literal)和符號引用(Symbolic Reference)。字面量比較接近Java語言層面的常量概念,如:文字字串、宣告為final的常量值等。

          而符號引用屬於編譯原理方面的概念,包括下面三類常量:1.類和介面的全限定名。2.欄位的名稱和描述符。3.方法的名稱和描述符。

          常量池有14種常量(JDK 7 之前有11種,JDK 7又加入了3種),每一個常量都是一張表。下面用一張表詳細展示常量池的內容。


          上面圖如果太小看不清,可以右鍵檢視原圖。通過上面的表格,可以很清楚的知道,常量池中的常量型別有哪些,分別表示什麼。舉個例子:CONSTANT_Utf8_info常量有三個欄位tag、length、bytes,其中tag標記常量型別,大小為u1,所有常量都有這個標記;length指定這個utf-8編碼的字串長度,bytes就是儲存utf-8編碼的字串。其實,字串型別的常量,欄位/方法簡單名、描述符字串都是儲存在這個常量裡。

          現在我們來看一個具體的例子:

import com.hexDemo;

public class Demo {
    private String str;
    public int execute() {
        return 0;
    }
}

          上圖是Demo類,經過編譯後產生的class檔案,使用十六進位制編輯器開啟後的位元組碼片段。首先,紅色框框就是表示的常量池入口,佔u2大小,這裡的值為0x0011,轉換成10進位制是17,也就說該類的常量池中有16個常量,注意:為什麼是16個常量不是17個?因為常量是從索引1開始的,索引0代表未指向任何常量。

          接著,我們看綠色框、紫色框(和棕色框),它們表示的就是一個具體的常量,上面用框框圈出了前四個常量。其中,綠色框表示的就是常量的tag標記,第一個0x0A代表tag為10的常量,通過查上面的常量表可以知道,tag為10的常量是CONSTANT_Methodref_info,緊接著後面的0x0003和0x000E分別表示指向宣告類描述符CONSTANT_Class_info的索引項和指向名稱及型別描述符CONSTANT_NameAndTyped的索引項。同樣,0x07000F和0x070010分別表示的是第二和第三個常量池常量。現在看看第四個,0x01代表著這個常量是CONSTANT_Utf8_info,0x0003代表接下去的3個位元組都是utf8編碼的字元,0x737472就是這三個字元,通過轉換成10進位制對應ascii碼,可以知道這三個字元為str,正好對應了原始碼中的屬性名。

        上面只圈出了四個常量,並且分析了其中兩個常量。如果要檢視類中所有的常量資訊,可以藉助JDK提供的javap命令,使用方法就是javap -verbose 包名.類名。具體看下圖。


訪問標誌access_flag(屬於class檔案的第六項)

常量池結束之後,緊接著的兩個位元組代表訪問標誌(access flag)。access_flag中一共有16個標誌位可用,當前只定義了其中8個,沒有使用到的標誌位要求一律為0。

          下標列出了類/介面的所有訪問標誌

          接著上面的例項:

          偏移量為0x000000AC,圖中紅色框框圈出的2個位元組,就代表著該類的訪問標記,即:0x0021,檢視上表可知,當前類的訪問標記為ACC_PUBLIC和ACC_SUPER。

類索引this_class、父類索引super_class、介面計數interfaces_count和介面集合interfaces(屬於class檔案的七到十項)

          在類訪問標誌後面,緊接著u2的類索引和u2的父類索引再加上u2的介面計數(為0的話,後面的介面集合佔位0),介面計數後面接著介面索引集合(每個介面索引也是u2)其中,類索引和父類索引用兩個u2型別的索引值表示,他們各自指向一個型別為CONSTANT_Class_info的類描述符常量,通過CONTSTANT_Class_info型別的常量中的索引值可以找到定義再CONSTANT_Utf8_info型別的常量中的全限定名字字串。

          同樣,我們接著看看例項:

          地址偏移0x0000AE,連續3個u2大小的項,0x0002、0x0003、0x0000代表的分別為類索引、父類索引和介面計數(因為當前類沒有實現任何介面,所以計數為0),其中0x0002為索引,值為2,指向常量池中第二個常量,可以發現這個常量是CONSTANT_Class_info型別,通過這個常量,指向了一個CONSTANT_Utf8_info的字串,這個字串就是當前類的全限定類名:com/hexDemo/Demo;同樣的,可以得出該類的直接父類為:java/lang/Object。

欄位表計數field_count和欄位表集合fields(屬於class檔案的十一和十二項)

          欄位表(field_info)用於描述介面或者類中宣告的變數。欄位(field)包括類級變數以及例項級變數,但不包括方法內部宣告的區域性變數。欄位的描述有哪些資訊?欄位的作用域(public、private、protected修飾符)、是例項變數還是類變數(static修飾符)、可變性(final)、併發可見性(volatile修飾符,是否強制從主記憶體讀寫)、可否被序列化(transient修飾符)、欄位資料型別(基本型別、物件、陣列)、欄位名稱。上述的這些資訊中,各個修飾符都是布林值,要麼有某個修飾符,要麼沒有,很適合使用標誌位來表示。而欄位叫什麼名字、欄位被定義為什麼資料型別這些都是無法固定的,只能引用常量池中的常量來描述。

          下圖展示了欄位表的結構

          欄位表包含了u2的訪問標誌、u2的名稱索引(指向常量池中的一個常量,對應該欄位的簡單名稱),u2的描述索引(也是指向常量池中的一個常量,對應該欄位的描述符),u2的屬性計數和屬性計數大小的屬性(attribute是附加的一些屬性資訊,在後面會詳細講)。

          在進行例項講解時,我們先看看欄位有哪些訪問標誌,下面同樣用一張表列出欄位的所有訪問標誌。需要注意:ACC_FINAL和ACC_VOLATILE不能同時為真,介面中ACC_PUBLIC、ACC_STATIC、ACC_FINAL必須同時為真。


          接著再解釋一下,什麼叫簡單名稱是指沒有型別和引數修飾的方法或欄位名,如上面給的原始碼類中,欄位名str和方法名execute就是簡單名稱。相對於簡單名稱,方法和欄位的描述符會稍微複雜點。

          這裡也不多說其它的,舉兩個例子就懂了。例1,用描述符描述一個 String[][] 欄位和 int[] 欄位 ,結果為:[[Ljava/lang/String和[I;

例2:用描述符描述一個 void execute(String[][] str, int i, int j, int[][] nums),結果為:([[Ljava/lang/StringII[[I)V,這裡需要注意,是引數型別在前,返回值型別再後。

          ps: void在虛擬機器規範中單獨列出為“voidDescriptor”,在《深入理解Java虛擬機器》一書中,作者為了結構統一,將void也列入表中。

          不能少的步驟,我們同樣進行一下例項分析:

          偏移量0x000000B4開始u2位元組,0x0001代表欄位表計數(這裡明顯為1,代表有一個欄位表),接著0x0002代表欄位表的第一項,即:access_flags,值為2,說明這個欄位是私有的,再接著0x0004代表一個欄位簡單名的索引,值為4,指向常量池中第4個的常量,值為str,就是該欄位的簡單名。0x0005,指向的是一個常量池索引,代表的是一個欄位的描述符,值為5,常量池中第五個常量為: Ljava/lang/String(抱歉,圖上忘記框起來了,偷點懶,就不重新畫了);地址偏移:0x000000BC,u2大小,0x0000代表屬性計數為0,所以到此欄位表集合結束。

方法表計數methods_count和方法表集合methods(屬於class檔案的第十三和第十四項)

          方法表的結構和欄位表結構是一模一樣的(這裡不在浪費版面,就不貼一樣的圖了),如果忘記了剛才介紹的欄位表結構,就稍微往前再看一遍(人的記憶有時候還不如金魚?)。

          方法的標誌值和欄位的標誌值還是有所區別,所以這裡也給出方法的所有標誌值。

          我想,看到這裡,應該所有人都懂的看class檔案的十六進位制編碼了,為了篇幅完整,我在這裡同樣給出例項。


          話不多說,0x0002方法計數,值為2,代表有兩個方法,怎麼會有兩個方法?別忘記除了例項方法execute外,還有例項構造器init()。第一個方法,訪問標誌0x0001,標明方法為public,0x0006簡單名索引,值為:<init>,0x0007方法描述符索引,值為:()V。

          到了這裡,我想大家一定有個疑問,方法體哪裡去了?不要忘記前面說的attributes表,方法體就是在attributes表的code項裡。


屬性表計數attributes_count和屬性表集合attributes(很多地方都有屬性表,比如:Class檔案、Class檔案的欄位表、方法表等)

        下面表中列出了屬性表的屬性型別(原諒我又偷懶了,沒有自己手動再畫一張表,下圖截自於《深入理解Java虛擬機器》)


          上表中列出了attributes表所可能出現的屬性,但需要注意,其中每個屬性都有一個表結構,表結構可能各不相同,但是每個屬性表的前面兩項都是一樣的。其中一個是u2大小的attribute_name_index,它指向常量池的一個CONSTANT_Utf8_info型別,代表一個屬性名;另外一個是u4大小的attribute_length,這一項用於說明該屬性的屬性表大小。

          下面將介紹屬性表中的幾個比較重要的屬性。

Code屬性(class檔案中的方法表中的attributes屬性表中的Code屬性)

          到現在,內容開始有點多了,不要搞混各個項所在的位置。Code屬性其實就是儲存類中方法的位元組碼,具體怎麼儲存?下面同樣給出Code屬性表的結構進行詳解。


          attribute_name_indexattribute_length就不多說了,就是一個常量池字串的引用和標記屬性長度的項而已。
          max_stack:代表運算元棧(Operand Stacks)的深度的最大值。虛擬機器執行時需要根據這個值來分配棧幀(Stack Frame)中的操作棧深度。
          max_locals:代表區域性變量表所需要的儲存空間,在這裡,max_locals的單位是Slot,Slot是虛擬機器為區域性變數分配記憶體所使用的最小單位。對於byte、char、float、int、short、boolean和returnAddress等長度不超過32位的資料型別,每個區域性變數佔用一個Slot,而double和long這兩種64位的資料型別則需要兩個Slot來存放。方法引數(包括例項方法中隱藏引數“this”)、顯式異常處理器的引數(Exception Handler Parameter,就是try-catch語句中catch塊所定義的異常)、方法體定義的區域性變數,這些都需要使用區域性變量表來存放。需要注意的是,不能把這些區域性變數所佔Slot之和作為max_locals的值,因為區域性變量表中的Slot可以重用,當代碼執行超出一個區域性變數的作用域時,這個區域性變數所佔的Slot可以被其它區域性變數所使用,Javac編譯器會根據區域性變數的作用域來分配Slot給各個變數使用,然後計算出max_locals的大小
           code_length:代表位元組碼長度,這是一個u4型別的長度值,理論上最大值可以達到2^23-1,但是虛擬機器規範  中限制了一個方法不允許超過65535條位元組碼指令。所以,實際上,它只使用了u2長度。
          code:用於儲存位元組碼指令的一系列位元組流,每個位元組指令是一個u1型別的單位元組,所以一共可以表示256(0~255)條指令。目前,Java虛擬機器規範定義了其中約200條編碼值對應的指令含義。
          exception_table_length:u2型別,表示異常長度。
          exception_table:異常表集合。下表顯示了異常表的結構。這幾個型別的含義是,如果當位元組碼在statrt_pc行到第end_pc行之間(不含第end_pc行)出現了型別位ctach_type或其子類的異常(catch_type為指向一個CONSTANT_Class_info型常量的索引),則轉到第handler_pc行繼續處理。當catch_type的值為0時,代表任意異常情況都需要轉向到handler_pc處繼續處理。注意:此處位元組碼的“行”是一種形象的描述,指的是位元組碼相對於方法體開始的偏移量,而不是Java原始碼的行號。
          ps: 編譯器使用異常表而不是簡單的跳轉命令來實現Java異常和Finally處理機制。


Eception屬性(Class檔案的方法表中的attributes表的Eception屬性)
        
  這裡的Exceptions屬性是在方法表中的與Code屬性平級的一項屬性(注意與異常表區分),Excptions屬性的作用是列舉出方法中可能丟擲的受查異常(CheckedExceptions)。
          attribute_name_indexattribute_length不再贅述(忘記的再往前看看)。
          number_of_exception:表示方法可能丟擲number_of_exceptions種受查異常,每一種受檢查遺產夠用exception_index_table表示。
          exception_index_table:是指向常量池中的CONSTANT_Class_info型常量的索引,代表了該受查異常的型別。

LineNumberTable屬性(Class檔案的方法表中的attribute表中的Code表中的attribute表的LineNumberTable屬性)
          LineNumberTable屬性用於描述Java原始碼行號與位元組碼行號(位元組碼的偏移量)之間的對應關係。它並不是執行時必須的屬性,但預設會生成到Class檔案中,可以在Javac中分別使用-g:none或-g:lines選項來取消或要求生成這項資訊。如果選擇不生成LineNumberTable屬性,對程式執行產生的最主要的影響就是當丟擲異常時,堆疊中將不會顯示出錯行號,並且在除錯程式的時候,也無法按照原始碼行來設定斷點。

          attribute_name_indexattribute_length不再贅述(忘記的再往前看看)。
          line_number_table:是一個數量為line_number_table_length、型別為Line_number_info的集合,line_number_info表包括了start_pc和line_number兩個u2型別的資料項,前者是位元組碼行號後者是Java原始碼行號

LocalVaribaleTable屬性(Class檔案的attributes表中的LocalVariableTable屬性)
       
   LocalVariableTable屬性用於描述棧幀中區域性變量表中的變數與java原始碼中定義的變數之間的關係,它也不是執行時必須的屬性,但預設會生成到Class檔案中,可以在Javac中分別使用-g:none或-g:vars選項來取消或要求生成這項資訊。如果沒有生成這項屬性,最大的影響就是當其他人引用這個方法時,所有的引數名稱都將會丟失,IDE將會使用諸如arg0、arg1之類的佔位符代替原有的引數名。
          attribute_name_indexattribute_length不再贅述(忘記的再往前看看)。
          local_varaibale_info:這個專案代表了一個棧幀與原始碼中區域性變數的關聯。下面再列出local_variable_info專案結構。

                    name_indexdescriptor:都是指向常量池中CONSTANT_Utf8_info型常量的索引,分別代表了局部變數的名稱和這個區域性變數的描述符。
                    start_pclength屬性:分別代表了這個區域性變數的宣告週期開始的位元組碼偏移量及其操作的範圍覆蓋的長度,兩者結合起來就是這個區域性變數再位元組碼之中的作用域訪問。

SourceFile屬性(Class檔案的attributes項的SourceFile屬性)
         
SourceFile屬性用於記錄生成這個Class檔案的原始碼檔名稱。這個屬性同樣是可選的,可以分別使用Javac的-g:none或-g:source選項來關閉或要求關閉這項資訊。在Java中,對於大多數的類來說,類名和檔名是一致的,但是有一些特殊情況(如內部類)例外。如果不生成這項屬性,當丟擲異常時,堆疊中將不會顯示出錯程式碼所屬的檔名,這個屬性是一個定長的屬性,其結構見下表。

          sourcefile_index:指向常量池中CONSTANT_Utf8_info型數量的索引,常量值是原始碼檔案的檔名。
ConstantValue屬性(Calss檔案的欄位表中的attributes表的ConstantValue屬性)
          ConstantValue屬性的作用是通知虛擬機器自動為靜態變數賦值,只有被static關鍵字修飾的變數(類變數)才可以使用這項屬性。對於非static型別的變數(也就是例項變數)的賦值是在例項構造器<init>方法中進行的,而對於類變數,則有兩種方式可以選擇,在類構造器<clinit>方法中,或者使用ConstantValue屬性。
          目前Sun Javac 編譯器的選擇是:如果同時使用final和static類修飾一個變數,並且這個變數的資料型別是基本型別或者java.lang.String的話,就生成ConstantValue屬性來進行初始化, 如果這個變數沒有被final修飾,或者並非基本型別及字串,則將會選擇在<clinit>方法中進行初始化。

          attribute_length:該 資料項值必須固定為2(ConstantValue屬性是定長屬性)
          constantvalue_index:該 資料項代表了常量池中一個字面量常量的引用,根據欄位型別不同,字面量可以是CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_String_info常量中的一種

InnerClasses屬性(Class檔案的attributes表的InnerClass屬性)
          InnerClasses屬性用於記錄內部類與宿主類之間的關聯,如果一個類中定義了內部類,那編譯器將會為它以及它所包含的內部類生成InnerClasses屬性。該屬性的結構見下表。
          資料項number_of_classes代表需要記錄多個內部類資訊,每個內部類的資訊都由一個inner_classes_info表進行描述。inner_classes_info表的結構見下表。

          inner_class_infoouter_class_info_index:都是指向常量池中CONSTANT_Class_info常量的索引,分別代表了內部類和宿主類的符號引用。
          inner_name_index:是指向常量池中CONSTANT_Utf8_info型常量的索引,代表了內部類的名稱,如果是匿名內部類,那麼該項的值為0。
          inner_class_access_flags:是內部類的訪問標誌,類似於類的access_flags,它的取值範圍見下表。


Deprecated及Synthetic屬性(這個屬性在類/欄位表/方法表中的attribute中都可以有)
         
Deprecated和Synthetic這兩個屬性都屬於標誌型別的布林屬性,只有存在有和沒有的區別,沒有屬性值的概念。
          Deprecated屬性用於表示某個類、欄位或者方法,已經被程式作者定位不再推薦使用,他可以在再程式碼中通過使用@deorecared註解進行設定。
          Synthetic屬性代表此欄位或者方法並不是由Java原始碼直接產生,而是由編譯器自行新增的。再JDK 1.5之後,標識一個類、欄位或者方法是是由編譯器自動產生的,也可以設定它們訪問標誌中的ACC_SYNTHETIC標誌位,其中最典型的例子就是Bridge_Method。所有由非使用者程式碼產生的類、方法及欄位都應當至少設定SYnthetic屬性和ACC_SYSNTHETIC標誌位中的一項,唯一的例外是例項構造器“<init>”方法和類構造器“<cinit>”方法
         
Deprecated和Synthetic屬性結構非常簡單,這裡就不再給出表,這兩個屬性的結構只包含了屬性必有的u2的attribute_name_index和u4的attribute_length,其中attribute_length為0x00000000,因為沒有任何屬性值需要設定。

StatckMapTable屬性(Class檔案的方法表中的attribute表中的Code表中的attribute表中的StackMapTable屬性)
         
StackMapTable屬性再JDK 1.6釋出後增加到了Class檔案規範中,他是一個複雜的變長屬性,位於Code屬性的屬性表中。這個屬性會在虛擬機器類載入的位元組碼驗證階段被新型別檢查驗證其(Type Checker)使用,目的在於代替以前比較消耗效能的基於資料流分析的型別推倒驗證器。
          StackMapTable屬性包含零至多個棧對映幀(Stack Map Frames),每個棧對映幀都顯式或隱式地代表了一個位元組碼偏移量,用於表示執行到該位元組碼時區域性變量表和運算元棧地驗證型別。型別檢查器會通過檢查目標方法地區域性變數和運算元棧所需要地來確定一段程式碼指令是否複合邏輯約束。StackMapTable屬性地結構見下表。
          在版本號大於或等於50.0的Class檔案中,如果方法地Code屬性中沒有附帶StackMapTable屬性,那就意味者它帶有一個隱式地StackMap屬性。這個StackMap屬性地作用等同於number_of_entries的值為0的StackMapTable屬性。一個方法的Code屬性最多隻能有一個StackMapTable屬性,否則將丟擲ClassFormatError異常。

Signature屬性(Class檔案/Class檔案的欄位表/方法表中的attributes表中的Singature屬性)
          Signature屬性在JDK 1.5釋出後增加到了Class檔案規範之中,它時一個可選的定長屬性,可以出現於類、欄位表和方法表結構的屬性表中。在JDK 1.5中大幅增強了Java語言的語法,在此之後,任何類、介面、初始化方法或成員的泛型簽名,如果包含了型別變數(Type Variables)或引數化型別(Parameterized Types),則Signature屬性會為它記錄泛型簽名信息(Java語言泛的型採用的是擦除法實現的偽泛型)。Singature屬性的結構見下表。


          signature_index:該項的值必須是一個對常量池的有效索引(常量池在該索引處的項必須是CONSTANT_Utf8_info結構),表示類簽名、方法型別簽名或欄位型別簽名。如果當前Signature屬性是類檔案的屬性,則這個結構表示型別簽名,如果當前Singature屬性是欄位表的屬性,則這個結構表示欄位型別簽名。

BootstrapMethods屬性(Class檔案的attributes表的BootstrapMethods屬性)
         
BootstrapMethods屬性在JDK .17釋出後增加到了Class檔案規範之中,它是以惡搞複雜的變長屬性,位於類檔案的屬性表中。這個屬性用於儲存invokedynamic指令應用的引導方法限定符。BootstrapMethods屬性的結構表如下
其中引用到的bootstrap_method結構表如下
          bootstrap_method_ref:bootstrap_method_ref項的值必須是一個對常量池的有效索引。常量池在該索引處的值必須是一個CONSTANT_MethodHandle_info結構。
          num_bootstrap_arguments[]:bootstrap_arguments[]陣列的每個成員必須是一個對常量池的有效索引。常量池在該索引處必須是下列結構一:CONSTANT_String_info、CONSTANT_Class_info、CONSTANT_Integer_info、CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_MethodHandle_info或CONSTANT_MethodType_info。


          至此,Class檔案的主要內容都已經介紹完畢了。