官术网_书友最值得收藏!

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工具的使用。

主站蜘蛛池模板: 五莲县| 随州市| 抚宁县| 旺苍县| 洛扎县| 于田县| 鸡西市| 建湖县| 乐业县| 梁山县| 确山县| 上思县| 修武县| 漠河县| 景宁| 玛多县| 巩义市| 辽源市| 苗栗县| 东兰县| 宜兴市| 湟中县| 平阳县| 石柱| 宁津县| 阿克苏市| 富平县| 鹰潭市| 静安区| 武定县| 明光市| 郑州市| 谢通门县| 江山市| 新野县| 陇西县| 巴彦县| 广丰县| 长宁县| 临夏市| 凯里市|