- 深入理解JVM字節碼
- 張亞
- 8885字
- 2020-06-02 18:27:52
1.2 class文件結構剖析
Java虛擬機規定用u1、u2、u4三種數據結構來表示1 、2、4字節無符號整數,相同類型的若干條數據集合用表(table)的形式來存儲。表是一個變長的結構,由代表長度的表頭n和緊隨著的n個數據項組成。class文件采用類似C語言的結構體來存儲數據,如下所示。
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 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; }
class文件由下面十個部分組成:
? 魔數(Magic Number)
? 版本號(Minor&Major Version)
? 常量池(Constant Pool)
? 類訪問標記(Access Flag)
? 類索引(This Class)
? 超類索引(Super Class)
? 接口表索引(Interface)
? 字段表(Field)
? 方法表(Method)
? 屬性表(Attribute)
Optimizing Java的作者編了一句順口溜幫忙記住上面這十部分:My Very Cute Animal Turns Savage In Full Moon Areas。如圖1-4所示。

圖1-4 class文件結構順口溜
1.2.1 魔數
人們經常通過文件名后綴來識別文件類型,比如看到一個.jpg后綴的文件,我們就知道這是一個jpg圖片文件。但使用文件名后綴來區分文件類型很不靠譜,后綴可以被隨便修改,那如何根據文件內容本身來標識文件的類型呢?可以用魔數(Magic Number)實現。
很多文件都以固定的幾字節開頭作為魔數,比如PDF文件的魔數是 %PDF-(十六進制0x255044462D), png文件的魔數是 \x89PNG(十六進制0 x89504E47)。文件格式的制定者可以自由地選擇魔數值,只要魔數值還沒有被廣泛采用過且不會引起混淆即可。
使用十六進制工具打開class文件,首先看到的是充滿浪漫氣息的魔數0xCAFEBABE(咖啡寶貝),從Java的圖標也可以看出,Java從誕生之初就和咖啡這個詞有千絲萬縷的關系。class文件的魔數如圖1-5所示。

圖1-5 class文件魔數
魔數0 xCAFEBABE是JVM識別.class文件的標志,虛擬機在加載類文件之前會先檢查這4個字節,如果不是0 xCAFEBABE,則會拋出java.lang.ClassFormatError異常。我們可以把前面的class文件的4個字節改為0 xCAFEBABA來模擬這種情況,使用Java運行這個修改過的class文件,會出現預期的異常,如圖1-6所示。

圖1-6 執行非法魔數的class文件效果
關于Java魔數的由來有這樣一段故事,Java之父James Gosling曾經寫過一篇文章,大意是他之前常去的一家飯店有一個叫Grateful Dead的樂隊出名前在此演出,后來樂隊的主唱不幸去世,他們就將這個地方稱為CAFEDEAD。當時Gosling正好在設計一些文件的編碼格式,需要兩個魔數,一個用于對象持久化,一個用于class文件,這兩個魔數有著共同的前綴CAFE,他選擇了CAFEDEAD作為對象持久化文件的魔數,選擇了CAFEBABE作為class文件的魔數。
1.2.2 版本號
在魔數之后的四個字節分別表示副版本號(Minor Version)和主版本號(Major Version),如圖1-7所示。

圖1-7 class文件版本號
這里的主版本號是52(0x34),虛擬機解析這個類時就知道這是一個Java 8編譯出的類,如果類文件的版本號高于JVM自身的版本號,加載該類會被直接拋出java.lang. UnsupportedClassVersionError異常,如圖1-8所示。

圖1-8 加載高版本class文件異常
每次Java發布大版本,主版本會加1,目前常用的Java主版本號對應的關系如表1-1所示。
表1-1 Java版本與Major Version的關系

1.2.3 常量池
緊隨版本號之后的是常量池數據區域,常量池是類文件中最復雜的數據結構。對于JVM字節碼來說,如果操作數是很常用的數字,比如0,這些操作數是內嵌到字節碼中的。如果是字符串常量和較大的整數等,class文件則會把這些操作數存儲在常量池(Constant Pool)中,當使用這些操作數時,會根據常量池的索引位置來查找。
常量池的作用類似于C語言中的符號表(Symbol Table),但是比符號表要強大很多。常量池結構如下面的代碼所示。
struct { u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; }
由上面的偽代碼可知,常量池分為兩部分。
1)常量池大小(cp_info_count):常量池是class文件中第一個出現的變長結構。既然是池,就有大小,常量池大小由兩個字節表示。假設常量池大小為 n,常量池真正有效的索引是1~n-1。也就是說,如果constant_pool_count等于10, constant_pool數組的有效索引值是1~9。0屬于保留索引,可供特殊情況使用。
2)常量池項(cp_info)集合:最多包含 n-1個元素。為什么是最多呢?long和double類型的常量會占用兩個索引位置,如果常量池包含了這兩種類型的元素,實際的常量池項的元素個數比 n-1要小。
常量池組成結構如圖1-9所示。

圖1-9 常量池組成結構
常量池中的每個常量項cp_info的數據結構如下面的偽代碼所示。
cp_info { u1 tag; u1 info[]; }
每個cp_info的第一個字節表示常量項的類型(tag),接下來的幾個字節表示常量項的具體內容。
Java虛擬機目前一共定義了14種常量項tag類型,這些常量名都以CONSTANT開頭,以info結尾,如表1-2所示。
表1-2 常量池類型

如果想查看類文件的常量池,可以在javap命令中加上 -v選項,如下所示。
javap -v HelloWorld Constant pool: #1 = Methodref #6.#15 // java/lang/Object."<init>":()V #2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #18 // Hello, World ... #27 = Utf8 println #28 = Utf8 (Ljava/lang/String; )V
接下來將逐一介紹上面的14種常量池類型。
1. CONSTANT_Integer_info和CONSTANT_Float_info
CONSTANT_Integer_info和CONSTANT_Float_info這兩種結構分別用來表示int和float類型的常量,兩者的結構很類似,都用4個字節來表示具體的數值常量,它們的結構定義如下所示。
CONSTANT_Integer_info { u1 tag; u4 bytes; } CONSTANT_Float_info { u1 tag; u4 bytes; }
以整型常量18(0x12)為例,它在常量池中的布局結構為如圖1-10所示。

圖1-10 整型常量項結構
其中第一個字節0 x03表示常量的類型為CONSTANT_Integer_info,接下來的四個字節是整型常量的值0 x12。
Java語言規范還定義了boolean、byte、short和char類型的變量,在常量池中都會被當作int來處理,以下面的代碼清單1-2為例。
代碼清單1-2 int整型常量表示
public class MyConstantTest { public final boolean bool = true; // 1(0x01) public final char c = 'A'; // 65(0x41) public final byte b = 66; // 66(0x42) public final short s = 67; // 67(0x43) public final int i = 68; // 68(0x44) }
編譯生成的整型常量在class文件中的位置如圖1-11所示。

圖1-11 整型常量項的表示
2. CONSTANT_Long_info和CONSTANT_Double_info
CONSTANT_Long_info和CONSTANT_Double_info這兩種結構分別用來表示long和double類型的常量,二者都用8個字節表示具體的常量數值,它們的結構如下面的代碼所示。
CONSTANT_Long_info { u1 tag; u4 high_bytes; u4 low_bytes; } CONSTANT_Double_info { u1 tag; u4 high_bytes; u4 low_bytes; }
以下面代碼中的long型常量a為例。
public class HelloWorldMain { public final long a = Long.MAX_VALUE; }
對應的結構如圖1-12所示。

圖1-12 long型常量項結構
其中第1個字節0 x05表示常量的類型為CONSTANT_Long_info,接下來的8個字節是long型常量的值Long.MAX_VALUE。
使用javap輸出的常量池信息如下所示。
Constant pool: #1 = Methodref #7.#17 // java/lang/Object."<init>":()V #2 = Class #18 // java/lang/Long #3 = Long 9223372036854775807l #5 = Fieldref #6.#19 // Hello.a:J // ... 省略部分常量項 #21 = Utf8 java/lang/Object
前面提到過,CONSTANT_Long_info和CONSTANT_Double_info占用兩個常量池位置,可以看到常量池大小為22,常量a占用了 #3和 #4兩個位置,下一個常量項Fieldref從索引值5開始,如圖1-13所示。

圖1-13 long型常量在常量池的位置
3. CONSTANT_Utf8_info
CONSTANT_Utf8_info存儲了字符串的內容,結構如下所示。
CONSTANT_Utf8_info { u1 tag; u2 length; u1 bytes[length]; }
它由三部分構成:第一個字節是tag,值為固定值1; tag之后的兩個字節length并不是表示字符串有多少個字符,而是表示第三部分byte數組的長度;第三部分是采用MUTF-8編碼的長度為length的字節數組。
如果要存儲的字符串是"hello",存儲結構如圖1-14所示。

圖1-14 UTF8類型常量項的結構
MUTF-8編碼與標準的UTF-8編碼在大部分情況下是相同的,但也有一些細微的區別,為了能搞清楚MUTF-8,需要知道UTF-8編碼是如何實現的。UTF-8是一種變長編碼方式,使用1~4個字節表示一個字符,規則如下。
1)對于傳統的ASCII編碼字符(0x0001~0x007F), UTF-8用一個字節來表示,如下所示。
00000001~0000007F -> 0xxxxxxx
因此英文字母的ASCII編碼和UTF-8編碼的結果一樣。
2)對于0080~07FF范圍的字符,UTF-8用2個字節來表示,如下所示。
00000080~0000 07FF -> 110xxxxx 10xxxxxx
程序在遇到這種字符的時候,會把第一個字節的110和第二個字節的10去掉,再把剩下的bit組成新的兩字節數據。
3)對于00000800~0000FFFF范圍的字符,UTF-8用3個字節表示,如下所示。
00000800~0000 FFFF -> 1110xxxx 10xxxxxx 10xxxxxx
程序在遇到這種字符的時候,會把第一個字節的1110、第二和第三個字節的10去掉,再把剩下的bit組成新的3字節數據。
4)對于00010000~0010FFFF范圍的字符,UTF-8用4個字節表示,如下所示。
00010000-0010 FFFF -> 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
程序在遇到這種字符的時候,會把第一個字節的11110以及第二、第三、第四個字節中的10去掉,再把剩下的位組成新的4字節數據。
以機械工業出版社的“機”字為例,它的unicode編碼為0 x673A(0110011100111010),在00000800~0000FFFF范圍內,根據上面的規則應該用3個字節表示,將對應的位填到空缺的x中,得到對應的UTF8編碼為0 xE69CBA,如圖1-15所示。

圖1-15 utf8編碼
那MUTF-8有什么不一樣呢?它們之間的區別如下。
1)MUTF-8里用兩個字節表示空字符("\0"),把前面介紹的雙字節表示格式110xxxxx 10xxxxxx中的x全部填0,也即0 xC080,而在標準UTF-8編碼中只用一個字節0 x00表示。這樣做的原因是在其他語言中(比如C語言)會把空字符當作字符串的結束,而MUTF-8這種處理空字符的方式保證字符串中不會出現空字符,在C語言處理時不會意外截斷。
2)MUTF-8只用到了標準UTF-8編碼中的單字節、雙字節、三字節表示方式,沒有用到4字節表示方式。編碼在U+FFFF之上的字符,Java使用“代理對”(surrogate pair)通過2個字符表示,比如emoji表情“”的代理對為\ud83d\ude02。
下面用一個實際例子來看MUTF-8編碼,代碼如下所示。
public final String x = "\0"; public final String y = "\uD83D\uDE02"; // emoji表情
上面代碼中常量x的值為空字符,常量y的值為emoji表情“”。編譯上面的代碼,使用十六進制工具查看,如圖1-16所示。

圖1-16 MUTF-8 編碼示例
可以看到x對應的空字符表示為010002 C080,其中第一個字節01表示CONSTANT_Utf8_info類型,緊隨其后的兩個字節0 x0002表示byte數組的長度,最后的兩個字節0 xC080印證了之前的描述。
y對應的emoji字符在class文件中表示為010006 ED A0 BD ED B882,第一個字節0x01表示常量項tag,緊隨其后的兩個字節表示byte數組的長度,這里為6,表示使用六個字節來表示這個emoji字符,接下來的6個字節是使用兩個3字節表示的UTF-8編碼,它的解碼過程如下。
前三個字節ED A0 BD對應的二進制為111011011010000010111101,根據UTF-8三字節表示方式,去掉第一個字節的1110、第二和第三個字節的10,剩下的位是1101100000111101,也即0 xD83D,同理可得剩下的3字節對應0 xDE02,得到這個emoji的編碼為4字節“0xD83D DE02”,對應的MUTF-8解碼過程如下所示。
1110 xxxx 10xx xxxx 10xx xxxx 111011011010000010111101 -> 1101 100000 111101-> D83D 111011011011100010000010 -> 1101 111000 000010-> DE02
4. CONSTANT_String_info
CONSTANT_String_info用來表示java.lang.String類型的常量對象。它與CONSTANT_Utf8_info的區別是CONSTANT_Utf8_info存儲了字符串真正的內容,而CONSTANT_String_info并不包含字符串的內容,僅僅包含一個指向常量池中CONSTANT_Utf8_info常量類型的索引。
CONSTANT_String_info的結構由兩部分構成,第一個字節是tag,值為8 , tag后面的兩個字節是一個名為string_index的索引值,指向常量池中的CONSTANT_Utf8_info,這個CONSTANT_Utf8_info中存儲的才是真正的字符串常量內容,如下所示。
CONSTANT_String_info { u1 tag; u2 string_index; }
以下面代碼中的字符串a為例。
public class Hello { private String a = "hello"; }
這一部分在class文件中對應的區域如圖1-17所示。

圖1-17 CONSTANT_String_info示例
對應的CONSTANT_String_info的存儲布局方式如圖1-18所示。

圖1-18 string類型常量項結構
5. CONSTANT_Class_info
CONSTANT_Class_info結構用來表示類或接口,它的結構與CONSTANT_String_info非常類似,可用下面的偽代碼表示。
CONSTANT_Class_info { u1 tag; u2 name_index; }
它由兩部分組成,第一個字節是tag,值固定為7 , tag后面的兩個字節name_index是一個常量池索引,指向CONSTANT_Utf8_info常量,這個字符串存儲的是類或接口的全限定名,如圖1-19所示。

圖1-19 class類型常量項的結構
6. CONSTANT_NameAndType_info
CONSTANT_NameAndType_info結構用來表示字段或者方法,可以用下面的偽代碼表示。
CONSTANT_NameAndType_info{ u1 tag; u2 name_index; u2 descriptor_index; }
CONSTANT_NameAndType_info結構由三部分組成,第一部分tag值固定為12,后面的兩個部分name_index和descriptor_index都指向常量池中的CONSTANT_Utf8_info的索引,name_index表示字段或方法的名字,descriptor_index是字段或方法的描述符,用來表示一個字段或方法的類型,字段和方法描述符在本章后面會有詳細介紹。
以下面代碼中的testMethod為例。
public void testMethod(int id, String name) { }
對應的CONSTANT_NameAndType_info的結構布局示意圖如圖1-20所示。

圖1-20 NameAndType類型常量項結構
7. CONSTANT_Fieldref_info、CONSTANT_Methodref_info和CONSTANT_InterfaceMethodref_info
這三種常量類型結構比較類似,結構用偽代碼表示如下。
CONSTANT_Fieldref_info { u1 tag; u2 class_index; u2 name_and_type_index; } CONSTANT_Methodref_info { u1 tag; u2 class_index; u2 name_and_type_index; } CONSTANT_InterfaceMethodref_info { u1 tag; u2 class_index; u2 name_and_type_index; }
下面以CONSTANT_Methodref_info為例來進行講解,它用來描述一個方法。它由三部分組成:第一部分是tag值,固定為10;第二部分是class_index,是一個指向CONSTANT_Class_info的常量池索引值,表示方法所在的類信息;第三部分是name_and_type_index,是一個指向CONSTANT_NameAndType_info的常量池索引值,表示方法的方法名、參數和返回值類型。以下面的代碼清單1-3為例。
代碼清單1-3 CONSTANT_Methodref_info代碼示例
public class HelloWorldMain { public static void main(String[] args) { new HelloWorldMain().testMethod(1, "hi"); } public void testMethod(int id, String name) { } } Constant pool: #2 = Class #18 // HelloWorldMain #5 = Methodref #2.#20 // HelloWorldMain.testMethod:(ILjava/lang/String; )V #20 = NameAndType #13:#14 // testMethod:(ILjava/lang/String; )V
testMethod對應的Methodref的class_index為2,指向類名為“HelloWorldMain”的類,name_and_type_index為20,指向常量池中下標為20的NameAndType索引項,對應的方法名為“testMethod”,方法類型為“(ILjava/lang/String;)V”。
testMethod的Methodref信息可以用圖1-21表示。

圖1-21 Methodref類型常量項結構
8. CONSTANT_MethodType_info、CONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info
從JDK1.7開始,為了更好地支持動態語言調用,新增了3種常量池類型(CONSTANT_MethodType_info、CONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info)。以CONSTANT_InvokeDynamic_info為例,CONSTANT_InvokeDynamic_info的主要作用是為invokedynamic指令提供啟動引導方法,它的結構如下所示。
CONSTANT_InvokeDynamic_info { u1 tag; u2 bootstrap_method_attr_index; u2 name_and_type_index; }
第一部分為tag,值固定為18;第二部分為bootstrap_method_attr_index,是指向引導方法表bootstrap_methods[] 數組的索引。第三部分為name_and_type_index,是指向索引類常量池里的CONSTANT_NameAndType_info的索引,表示方法描述符。以下面的代碼清單1-4為例。
代碼清單1-4 CONSTANT_InvokeDynamic_info代碼示例
public void foo() { new Thread (()-> { System.out.println("hello"); }).start(); } javap輸出的常量池的部分如下: Constant pool: #3 = InvokeDynamic #0:#25 // #0:run:()Ljava/lang/Runnable; ... #25 = NameAndType #37:#38 // run:()Ljava/lang/Runnable; BootstrapMethods: 0: #22 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/ invoke/MethodHandles$Lookup; Ljava/lang/String; Ljava/lang/invoke/MethodType; Ljava/ lang/invoke/MethodType; Ljava/lang/invoke/MethodHandle; Ljava/lang/invoke/MethodType; ) Ljava/lang/invoke/CallSite; Method arguments: #23 ()V #24 invokestatic HelloWorldMain.lambda$foo$0:()V #23 ()V
整體的結構如圖1-22所示。

圖1-22 InvokeDynamic類型常量項結構
至此,關于class文件最復雜的常量池部分的介紹就告一段落,接下來我們將繼續講解class文件剩下的幾個部分。
1.2.4 Access flags
緊隨常量池之后的區域是訪問標記(Access flags),用來標識一個類為final、abstract等,由兩個字節表示,總共有16個標記位可供使用,目前只使用了其中的8個,如圖1-23所示。

圖1-23 類訪問標記
完整的訪問標記含義如表1-3所示。
表1-3 類訪問標記

本例中類的訪問標記為0 x0021(ACC_SUPER | ACC_PUBLIC),表示是一個public的類,如圖1-24所示。

圖1-24 類訪問標記
這些訪問標記并不是可以隨意組合的,比如ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED不能同時設置,ACC_FINAL和ACC_ABSTRACT也不能同時設置,否則會違背語義。更多的規則可以在javac源碼的com.sun.tools.javac.comp.Check.java文件中找到。
1.2.5 this_class、super_name、interfaces
這三部分用來確定類的繼承關系,this_class表示類索引,super_name表示直接父類的索引,interfaces表示類或者接口的直接父接口。
this_class是一個指向常量池的索引,表示類或者接口的名字,用兩字節表示,以下面的代碼清單1-5為例。
代碼清單1-5 this_class代碼示例
public class Hello { public static void main(String[] args) { } } Constant pool: // ... #2 = Class #13 // Hello // ... #13 = Utf8 Hello
本例中this_class為0 x0002,指向常量池中下標為2的元素,這個元素是CONSTANT_Class_info類型,它的name_index指向常量池中下標為13、類型為CONSTANT_Utf8_info的元素,表示類名為“Hello”,如圖1-25所示。

圖1-25 this_class分析
super_class和interfaces的原理與之類似,不再贅述。接下來開始介紹字段表。
1.2.6 字段表
緊隨接口索引表之后的是字段表(fields),類中定義的字段會被存儲到這個集合中,包括靜態和非靜態的字段,它的結構可以用下面的偽代碼表示。
{ u2 fields_count; field_info fields[fields_count]; }
字段表也是一個變長的結構,fields_count表示field的數量,接下來的fields表示字段集合,共有fields_count個,每一個字段用field_info結構表示,稍后會進行介紹。
1.字段field_info結構
每個字段field_info的格式如下所示。
field_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
字段結構分為4個部分:第一部分access_flags表示字段的訪問標記,用來標識是public、private還是protected,是否是static,是否是final等;第二部分name_index用來表示字段名,指向常量池的字符串常量;第三部分descriptor_index是字段描述符的索引,指向常量池的字符串常量;最后的attributes_count、attribute_info表示屬性的個數和屬性集合。如圖1-26所示。

圖1-26 field_info組成結構示意
接下來會詳細介紹這些組成部分。
2.字段訪問標記
與類一樣,字段也擁有自己的字段訪問標記,但字段的訪問標記更豐富,共有9種,詳細的列表如表1-4所示。
表1-4 字段訪問標記

如果在類中定義了字段public static final int DEFAULT_SIZE = 128,編譯后DEFAULT_SIZE字段在類文件中存儲的訪問標記值為0 x0019,則它的訪問標記為ACC_PUBLIC |ACC_STATIC | ACC_FINAL,表示它是一個public static final類型的變量,如圖1-27所示。

圖1-27 字段訪問標記示例
同之前介紹的類訪問標記一樣,字段訪問標記并不是可以隨意組合的,比如ACC_FINAL和ACC_VOLATILE也不能同時設置,否則會違背語義。
3.字段描述符
字段描述符(field descriptor)用來表示某個field的類型,在JVM中定義一個int類型的字段時,類文件中存儲的類型并不是字符串int,而是更精簡的字母I。
根據類型的不同,字段描述符分為三大類。
1)原始類型,byte、int、char、float等這些簡單類型使用一個字符來表示,比如J對應long類型,B對應byte類型。
2)引用類型使用L;的方式來表示,為了防止多個連續的引用類型描述符出現混淆,引用類型描述符最后都加了一個“;”作為結束,比如字符串類型String的描述符為“Ljava/lang/String;”。
3)JVM使用一個前置的“[”來表示數組類型,如int[] 類型的描述符為“[I”,字符串數組String[] 的描述符為“[Ljava/lang/String;”。而多維數組描述符只是多加了幾個“[”而已,比如Object[][][] 類型的描述符為“[[[Ljava/lang/Object;”。
完整的字段類型描述符映射表如表1-5所示。
表1-5 字段類型描述符映射表

4.字段屬性
與字段相關的屬性包括ConstantValue、Synthetic 、Signature、Deprecated、Runtime-Visible Annotations和RuntimeInvisibleAnnotations這6個,比較常見的是ConstantValue屬性,用來表示一個常量字段的值,具體將在1.2.8節展開介紹。
1.2.7 方法表
方法表的作用與前面介紹的字段表非常類似,類中定義的方法會被存儲在這里,方法表也是一個變長結構,如下所示。
{ u2 methods_count; method_info methods[methods_count]; }
其中methods_count表示方法的數量,接下來的methods表示方法的集合,共有methods_count個,每一個方法用method_info結構表示。
1.方法method_info結構
對于每個方法method_info而言,它的結構如下所示。
method_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
方法method_info結構分為四部分:第一部分access_flags表示方法的訪問標記,用來標記是public、private還是protected,是否是static,是否是final等;接下來的name_index、descriptor_index分別表示方法名和方法描述符的索引值,指向常量池的字符串常量;attributes_count和attribute_info表示方法相關屬性的個數和屬性集合,包含了很多有用的信息,比如方法內部的字節碼就存放在Code屬性中。
field_info的結構如圖1-28所示。

圖1-28 field_info結構
2.方法訪問標記
方法的訪問標記比類和字段的訪問標記類型更豐富,一共有12種,完整的映射表如表1-6所示。
表1-6 方法訪問標記映射表

以下面的代碼為例:
private static synchronized void foo() { }
生成的類文件中,foo方法的訪問標記等于0 x002a(ACC_PRIVATE | ACC_STATIC|ACC_SYNCHRONIZED),表示這是一個private static synchronized的方法,如圖1-29所示。

圖1-29 方法訪問標記
同前面的字段訪問標記一樣,不是所有的方法訪問標記都可以隨意組合設置,比如ACC_ABSTRACT、ACC_FINAL在方法描述符中不能同時設置,ACC_ABSTRACT和ACC_SYNCHRONIZED也不能同時設置。
3.方法名與描述符
緊隨方法訪問標記的是方法名索引name_index,指向常量池中CONSTANT_Utf8_info類型的字符串常量,比如有這樣一個方法定義private void foo(),編譯器會生成一個類型為CONSTANT_Utf8_info的字符串常量項,里面存儲了“foo”,方法名索引name_index指向了這個常量項。
方法描述符索引descriptor_index也是指向常量池中類型為CONSTANT_Utf8_info的字符串常量項。方法描述符用來表示一個方法所需的參數和返回值,格式如下:
(參數1類型 參數2類型 參數3類型 ...)返回值類型
比如,方法Object foo(int i, double d, Thread t)的描述符為“(IDLjava/lang/Thread;)Ljava/lang/Object;”,其中“I”表示第一個參數i的參數類型int,“D”表示第二個參數d的類型double,“Ljava/lang/Thread;”表示第三個參數t的類型Thread,“Ljava/lang/Object;”表示返回值類型Object,如圖1-30所示。

圖1-30 方法描述符
4.方法屬性表
方法屬性表是method_info結構的最后一部分。前面介紹了方法的訪問標記和方法簽名,還有一些重要的信息沒有出現,如方法聲明拋出的異常,方法的字節碼,方法是否被標記為deprecated等,屬性表就是用來存儲這些信息的。與方法相關的屬性有很多,其中比較重要的是Code和Exceptions屬性,其中Code屬性存放方法體的字節碼指令,Exceptions屬性用于存儲方法聲明拋出的異常。屬性的細節我們將在1.2.8節中進行介紹。
1.2.8 屬性表
在方法表之后的結構是class文件的最后一部分——屬性表。屬性出現的地方比較廣泛,不只出現在字段和方法中,在頂層的class文件中也會出現。相比于常量池只有14種固定的類型,屬性表的類型更加靈活,不同的虛擬機實現廠商可以自定義屬性,屬性表的結構如下所示。
{ u2 attributes_count; attribute_info attributes[attributes_count]; }
與其他結構類似,屬性表使用兩個字節表示屬性的個數attributes_count,接下來是若干個屬性項的集合,可以看作是一個數組,數組的每一項都是一個屬性項attribute_info,數組的大小為attributes_count。每個屬性項的attribute_info的結構如下所示。
attribute_info{ u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length]; }
attribute_name_index是指向常量池的索引,根據這個索引可以得到attribute的名字,接下來的兩部分表示info數組的長度和具體byte數組的內容。
虛擬機預定義了20多種屬性,下面我們挑選字段表相關的ConstantValue屬性和方法表相關的Code屬性進行介紹。
1. ConstantValue屬性
ConstantValue屬性出現在字段field_info中,用來表示靜態變量的初始值,它的結構如下所示。
ConstantValue_attribute { u2 attribute_name_index; u4 attribute_length; u2 constantvalue_index; }
其中attribute_name_index是指向常量池中值為“ConstantValue”的字符串常量項,attribute_length值固定為2,因為接下來的具體內容只會有兩個字節大小。constantvalue_index指向常量池中具體的常量值索引,根據變量的類型不同,constantvalue_index指向不同的常量項。如果變量為long類型,則constantvalue_index指向CONSTANT_Long_info類型的常量項。
以代碼public static final int DEFAULT_SIZE = 128為例,字段對應的class文件如圖1-31高亮部分所示。

圖1-31 字段DEFAULT_SIZE在class文件中的表示
它對應的字段結構如圖1-32所示。

圖1-32 完整的field_info字段結構
2. Code屬性
Code屬性是類文件中最重要的組成部分,它包含方法的字節碼,除native和abstract方法以外,每個method都有且僅有一個Code屬性,它的結構如下。
Code_attribute { u2 attribute_name_index; u4 attribute_length; u2 max_stack; u2 max_locals; u4 code_length; u1 code[code_length]; u2 exception_table_length; { u2 start_pc; u2 end_pc; u2 handler_pc; u2 catch_type; } exception_table[exception_table_length]; u2 attributes_count; attribute_info attributes[attributes_count]; }
下面開始介紹Code屬性表的各個字段含義。
1)屬性名索引(attribute_name_index)占2個字節,指向常量池中CONSTANT_Utf8_info常量,表示屬性的名字,比如這里對應的常量池的字符串常量“Code”。
2)屬性長度(attribute_length)占用2個字節,表示屬性值長度大小。
3)max_stack表示操作數棧的最大深度,方法執行的任意期間操作數棧的深度都不會超過這個值。它的計算規則是:有入棧的指令stack增加,有出棧的指令stack減少,在整個過程中stack的最大值就是max_stack的值,增加和減少的值一般都是1,但也有例外:LONG和DOUBLE相關的指令入棧stack會增加2 , VOID相關的指令則為0。
4)max_locals表示局部變量表的大小,它的值并不等于方法中所有局部變量的數量之和。當一個局部作用域結束,它內部的局部變量占用的位置就可以被接下來的局部變量復用了。
5)code_length和code用來表示字節碼相關的信息。其中,code_length表示字節碼指令的長度,占用4個字節;code是一個長度為code_length的字節數組,存儲真正的字節碼指令。
6)exception_table_length和exception_table用來表示代碼內部的異常表信息,如我們熟知的try-catch語法就會生成對應的異常表。exception_table_length表示接下來exception_table數組的長度,每個異常項包含四個部分,可以用下面的結構表示。
{ u2 start_pc; u2 end_pc; u2 handler_pc; u2 catch_type; }
其中start_pc、end_pc、handler_pc都是指向code字節數組的索引值,start_pc和end_pc表示異常處理器覆蓋的字節碼開始和結束的位置,是左閉右開區間 [start_pc, end_pc),包含start_pc,不包含end_pc。handler_pc表示異常處理handler在code字節數組的起始位置,異常被捕獲以后該跳轉到何處繼續執行。
catch_type表示需要處理的catch的異常類型是什么,它用兩個字節表示,指向常量池中類型為CONSTANT_Class_info的常量項。如果catch_type等于0,則表示可處理任意異常,可用來實現finally語義。
當JVM執行到這個方法 [start_pc, end_pc)范圍內的字節碼發生異常時,如果發生的異常是這個catch_type對應的異常類或者它的子類,則跳轉到code字節數組handler_pc處繼續處理。
7)attributes_count和attributes[] 用來表示Code屬性相關的附屬屬性,Java虛擬機規定Code屬性只能包含這四種可選屬性:LineNumberTable、LocalVariableTable、LocalVariableTypeTable、StackMapTable。以LineNumberTable為例,LineNumberTable用來存放源碼行號和字節碼偏移量之間的對應關系,屬于調試信息,不是類文件運行的必需屬性,默認情況下都會生成。如果沒有這個屬性,那么在調試時就沒有辦法在源碼中設置斷點,也沒有辦法在代碼拋出異常時在錯誤堆棧中顯示出錯的行號信息。
接下來以代碼清單1-6為例來看Code屬性。
代碼清單1-6 Code屬性代碼示例
public class HelloWorldMain { public static void main(String[] args) { try { foo(); } catch (NullPointerException e) { System.out.println(e); } catch (IOException e) { System.out.println(e); } try { foo(); } catch (Exception e) { System.out.println(e); } } public static void foo() throws IOException { } }
編譯后使用十六進制工具查看Code區域,如圖1-33所示。

圖1-33 Code屬性布局
其中attribute_name_index為0 x0C,指向常量池中下標為12的字符串“Code”。attribute_length等于154(0x9A),表示屬性值的長度大小。max_stack和max_locals都等于2,表示最大棧深度和局部變量表的大小都等于2 , code_length等于40(0x28),表示接下來code字節數組的長度為40。exception_table_length等于3(0x03),表示接下來會有3個異常表項目。最后的attributes_count為2,表示接下來會有2個相關的屬性項,這里是LineNumberTable和StackMapTable。根據前面的介紹,可以畫出的Code屬性結構如圖1-34所示。

圖1-34 Code屬性結構
至此,類文件的基本結構就介紹得差不多了,在結束本章之前,我們來看看javap工具的使用。
- 數據庫系統原理及MySQL應用教程(第2版)
- TypeScript入門與實戰
- C語言程序設計(第2 版)
- Vue.js 2 and Bootstrap 4 Web Development
- FFmpeg入門詳解:音視頻流媒體播放器原理及應用
- 數據結構簡明教程(第2版)微課版
- Java高并發核心編程(卷1):NIO、Netty、Redis、ZooKeeper
- 動手學數據結構與算法
- Frank Kane's Taming Big Data with Apache Spark and Python
- ArcGIS for Desktop Cookbook
- Extending Unity with Editor Scripting
- Python Machine Learning Blueprints:Intuitive data projects you can relate to
- Machine Learning for Developers
- Learning Bootstrap 4(Second Edition)
- Java 9 with JShell