Java 虛擬機中定義的 Class 文件格式。每一個 Class 文件都對應著唯一一個類 或介面的定義信息,但是相對地,類或介面並不一定都得定義在文件里(譬如類或介面也可以通過 類載入器直接生成)。本章中,我們只是通俗地將任意一個有效的類或介面所應當滿足的格式稱為 “Class 文件格式”,即使它 ...
Java 虛擬機中定義的 Class 文件格式。每一個 Class 文件都對應著唯一一個類 或介面的定義信息,但是相對地,類或介面並不一定都得定義在文件里(譬如類或介面也可以通過 類載入器直接生成)。本章中,我們只是通俗地將任意一個有效的類或介面所應當滿足的格式稱為 “Class 文件格式”,即使它不一定以磁碟文件的形式存在。
每個 Class 文件都是由 8 位元組為單位的位元組流組成,所有的 16 位、32 位和 64 位長度的數 據將被構造成 2 個、4 個和 8 個 8 位元組單位來表示。多位元組數據項總是按照 Big-Endian1的順 序進行存儲。在Java SDK中,訪問這種格式的數據可以使用java.io.DataInput、 java.io.DataOutput 等介面和 java.io.DataInputStream 和 java.io.DataOutputStream 等類來實現。
Class 文件的內容可用一組私有數據類型來表示,它們包括 u1,u2 和 u4,分別代 表了1、2和4個位元組的無符號數。在 Java SDK 中這些類型的數據可以通過實現介面 java.io.DataInput 中的 readUnsignedByte、readUnsignedShort 和 readInt 方法進 行讀取。
ClassFile 結構 每一個 Class 文件對應於一個如下所示的 ClassFile 結構體,其包含的屬性如下表:
看到上表,很多人就會懵逼,這些都是啥啊,後面我會根據一個示例來講解這些特性。
下麵我們來看一個簡單的 java 類,就是一個輸出了一個 hello world。
package com.hello.test; public class Log { public static void main(String[] args) { System.out.println("hello world!"); } }
輸入命令 javac Log.java 將其編譯成 class 文件後,打開:
cafe babe 0000 0034 001d 0a00 0600 0f09 0010 0011 0800 120a 0013 0014 0700 1507 0016 0100 063c 696e 6974 3e01 0003 2829 5601 0004 436f 6465 0100 0f4c 696e 654e 756d 6265 7254 6162 6c65 0100 046d 6169 6e01 0016 285b 4c6a 6176 612f 6c61 6e67 2f53 7472 696e 673b 2956 0100 0a53 6f75 7263 6546 696c 6501 0008 4c6f 672e 6a61 7661 0c00 0700 0807 0017 0c00 1800 1901 000c 6865 6c6c 6f20 776f 726c 6421 0700 1a0c 001b 001c 0100 1263 6f6d 2f68 656c 6c6f 2f74 6573 742f 4c6f 6701 0010 6a61 7661 2f6c 616e 672f 4f62 6a65 6374 0100 106a 6176 612f 6c61 6e67 2f53 7973 7465 6d01 0003 6f75 7401 0015 4c6a 6176 612f 696f 2f50 7269 6e74 5374 7265 616d 3b01 0013 6a61 7661 2f69 6f2f 5072 696e 7453 7472 6561 6d01 0007 7072 696e 746c 6e01 0015 284c 6a61 7661 2f6c 616e 672f 5374 7269 6e67 3b29 5600 2100 0500 0600 0000 0000 0200 0100 0700 0800 0100 0900 0000 1d00 0100 0100 0000 052a b700 01b1 0000 0001 000a 0000 0006 0001 0000 0003 0009 000b 000c 0001 0009 0000 0025 0002 0001 0000 0009 b200 0212 03b6 0004 b100 0000 0100 0a00 0000 0a00 0200 0000 0500 0800 0600 0100 0d00 0000 0200 0e
下麵將根據 class 文件來分析每一個位元組所代表的含義。
1. magic 魔數
Class 文件的第 1 - 4 個位元組代表了該文件的魔數。
魔數的唯一作用是確定這個文件是否為一個能被虛擬機所接受的 Class 文件。魔數值固定為 0xCAFEBABE,不會改變。
2. minor_version、major_version
Class 文件的第 5 - 6 個位元組代表了 Class 文件的副版本號。
Class 文件的第 7 - 8 個位元組代表了 Class 文件的主版本號。
副版本號和主版本號,minor_version 和 major_version 的值分別表示 Class 文件 的副、主版本。它們共同構成了 Class 文件的格式版本號。譬如某個 Class 文件的主版本號為 M,副版本號為 m,那麼這個 Class 文件的格式版本號就確定為 M.m。Class 文件格式版本號大小的順序為:1.5 < 2.0 < 2.1。
一個 Java 虛擬機實例只能支持特定範圍內的主版本號(Mi 至 Mj)和 0 至特定範圍 內(0 至 m)的副版本號。假設一個 Class 文件的格式版本號為 V,僅當 Mi.0 ≤ v ≤ Mj.m 成立時,這個 Class 文件才可以被此 Java 虛擬機支持。不同版本的 Java 虛擬機實現 支持的版本號也不同,高版本號的 Java 虛擬機實現可以支持低版本號的 Class 文件。
下表列出了各個版本 JDK 的十六進位版本號信息:
上述 class 文件 0000 0034 對應的就是表格中的 JDK1.8。
3. 常量池集合
3.1 constant_pool_count 常量池計數器
緊跟版本信息之後的是常量池信息,其中前 2 個位元組表示常量池計數器,其後的不定長數據則表示常量池的具體信息。
constant_pool_count 的值等於 constant_pool 表中的成員數加 1。 constant_pool 表的索引值只有在大於 0 且小於 constant_pool_count 時才會被認為是有效的,對於 long 和 double 類型有例外情況。在 Class 文件的常量池中,所有的 8 位元組的常量都占兩個表成員(項)的空間。如果一個 CONSTANT_Long_info 或 CONSTANT_Double_info 結構的項在常量池中的索引為 n,則常量池中下一個有效的項的索引為 n+2,此時常量池中索引為 n+1 的項有效但必須被認為不可用。
class 文件位元組碼對應的內容是:001d
,其值為 29,表示一共有 29 - 1 = 28 個常量。
3.2 constant_pool[]
緊跟著常量池計數器後面就是 28 個常量池了,因為每個常量都對應不同的類型,需要一個個具體分析。
常量池,constant_pool 是一種表結構,它包含 Class 文件結構及其子結構 中引用的所有字元串常量、類或介面名、欄位名和其它常量。常量池中的每一項都具備相 同的格式特征——第一個位元組作為類型標記用於識別該項是哪種類型的常量,稱為 "tag byte"。常量池的索引範圍是 1 至 constant_pool_count − 1。
所有的常量池項都具有如下通用格式:
cp_info {
u1 tag;
u1 info[];
}
常量池中,每個 cp_info 項的格式必須相同,它們都以一個表示 cp_info 類型的單位元組 “tag”項開頭。後面 info[]項的內容 tag 由的類型所決定。tag 有效的類型和對應的取值在表 4.3 列出。每個 tag 項必須跟隨 2 個或更多的位元組,這些位元組用於給定這個常量的信息,附加位元組的信息格式由 tag 的值決定。
在 Java 虛擬機規範中一共有 14 種 cp_info
類型的表結構。
而上面這些 cp_info
表結構又有不同的數據結構,其對應的數據結構如下圖所示。
接下來我們開始分析上述 Log.class 文件每個位元組的含義,前面第一句話已經說了,緊跟著常量池計數器後面的就是常量池了。下麵開始分析:
第 1 個常量
緊接著 001d 的後一個位元組為 0A,為十進位數字 10,查表可知其為方法引用類型(CONSTANT_Methodref_info)的常量。在 cp_info 中結構如下所示:
查找的方式是先確定 tag 值,根據 tag 值判斷當前屬於哪一個常量。這裡 tag 為 10,查表即可知是上圖。
然後看其結構顯示還有兩個 U2 的index,說明後面 4 個位元組都是屬於第一個常量,其中第 2 - 3 個位元組表示類信息,第 4 - 5 個位元組表示名稱及類描述符。
接下來我們取出這部分的數據:0a 0600 000f :
該常量項第 2 - 3 個位元組,其值為 00 06,表示指向常量池第 6 個常量所表示的信息。根據後面我們分析的結果知道第 6 個常量是 java/lang/Object
。第 4 - 5 個位元組,其值為 000f,表示指向常量池第 15 個常量所表示的信息,根據 javap 反編譯出來的信息可知第 15 個常量是 <init>:()V
。將這兩者組合起來就是:java/lang/Object.<init>:V
,即 Object 的 init 初始化方法。
下麵是輸入
javap -v Log.class
反編譯出來的結果:
javap -v Log.class Classfile /Users/xxx/Desktop/Log.class Last modified 2020-1-8; size 427 bytes MD5 checksum 745be5a6df4d9554e783dbbcecaf9b6d Compiled from "Log.java" public class com.hello.test.Log minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER 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! #4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = Class #21 // com/hello/test/Log #6 = Class #22 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 main #12 = Utf8 ([Ljava/lang/String;)V #13 = Utf8 SourceFile #14 = Utf8 Log.java #15 = NameAndType #7:#8 // "<init>":()V #16 = Class #23 // java/lang/System #17 = NameAndType #24:#25 // out:Ljava/io/PrintStream; #18 = Utf8 hello world! #19 = Class #26 // java/io/PrintStream #20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V #21 = Utf8 com/hello/test/Log #22 = Utf8 java/lang/Object #23 = Utf8 java/lang/System #24 = Utf8 out #25 = Utf8 Ljava/io/PrintStream; #26 = Utf8 java/io/PrintStream #27 = Utf8 println #28 = Utf8 (Ljava/lang/String;)V { public com.hello.test.Log(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String hello world! 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 5: 0 line 6: 8 } SourceFile: "Log.java"
其實從上面的結果也可以看出來,第一個常量對應的是第6,15個常量,組合起來的含義後面註釋也寫著了。
其他很多常量都是類似的,接下來我們看看字元串是怎麼來得。
第 21 個常量
第 21 個常量,數據為
0100 1263 6f6d 2f68 656c 6c6f 2f74 6573 742f 4c6f 67
這裡 tag 值是 01,對應的結構如下:
length 是 u2,對應著 0012,說明後面跟著 18 個位元組:63 6f6d 2f68 656c 6c6f 2f74 6573 742f 4c6f 67;查 ASCII 表可得 63-c, 6f-o, 6d-m, 2f-/ ··· 4c-L,6f-o, 67-g,
組合起來就是:com/hello/test/Log 。
相信通過上面兩個例子,大家就知道如何去分析常量池裡面的索引了。但很多時候我們可以藉助 JDK 提供的 javap 命令直接查看 Class 文件的常量池信息,但是手動分析能夠讓你更加瞭解結果為啥是這樣的。其實 javap 出來的就是人家分析總結好的。
4. access_flags 訪問標誌
在常量池結束之後,緊接著的兩個位元組代表類或介面的訪問標記(access_flags)。這裡的數據為 00 21。
access_flags 是一種掩碼標誌,用於表示某個類或者介面的訪問許可權及基礎屬性。access_flags 的取值範圍和相應含義見下表:
第一列是標記名;
第二列是對應的值;
第三列是對應的說明。
-
帶有 ACC_SYNTHETIC 標誌的類,意味著它是由編譯器自己產生的而不是由程式員 編寫的源代碼生成的。
-
帶有 ACC_ENUM 標誌的類,意味著它或它的父類被聲明為枚舉類型。
-
帶有 ACC_INTERFACE 標誌的類,意味著它是介面而不是類,反之是類而不是介面。
如果一個 Class 文件被設置了 ACC_INTERFACE 標誌,那麼同時也得設置 ACC_ABSTRACT 標誌。同時它不能再設置ACC_FINAL、 ACC_SUPER 和 ACC_ENUM 標誌。
-
註解類型必定帶有 ACC_ANNOTATION 標記,如果設置了 ANNOTATION 標記, ACC_INTERFACE 也必須被同時設置。如果沒有同時設置 ACC_INTERFACE 標記, 那麼這個 Class 文件可以具有表 4.1 中的除 ACC_ANNOTATION 外的所有其它標記。 當然 ACC_FINAL 和 ACC_ABSTRACT 這類互斥的標記除外。
-
ACC_SUPER 標誌用於確定該 Class 文件裡面的 invokespecial 指令使用的是哪 一種執行語義。目前 Java 虛擬機的編譯器都應當設置這個標誌。ACC_SUPER 標記 是為了向後相容舊編譯器編譯的 Class 文件而存在的,在 JDK1.0.2 版本以前的編 譯器產生的 Class 文件中,access_flag 裡面沒有 ACC_SUPER 標誌。同時, JDK1.0.2 前的 Java 虛擬機遇到 ACC_SUPER 標記會自動忽略它。
- 在表中沒有使用的 access_flags 標誌位是為未來擴充而預留的,這些預留的標誌為在編譯器中會被設置為 0, Java 虛擬機實現也會自動忽略它們。
5. 類索引、父類索引、介面索引
在訪問標記後,則是類索引、父類索引、介面索引的數據,這裡數據為:00 05 、00 06 、00 00。
類索引和父類索引都是一個 u2 類型的數據,而介面索引集合是一組 u2 類型的數據的集合,這個可以由前面 Class 文件的構成可以得到。Class 文件中由這三項數據來確定這個類的繼承關係。
5.1 this_class 類索引
類索引,this_class 的值必須是對 constant_pool 表中項目的一個有效索引值。 constant_pool 表在這個索引處的項必須為 CONSTANT_Class_info 類型常量,表示這個 Class 文件所定義的類或介面。這裡的類索引是 00 05 表示其指向了常量池中第 5 個常量,通過我們之前的分析,我們知道第 5 個常量其最終的信息是 Log 類。
5.2 super_class 父類索引
對於類來說,super_class 的值必須為 0 或者是對 constant_pool 表中 項目的一個有效索引值。如果它的值不為 0,那 constant_pool 表在這個索引處的項 必須為 CONSTANT_Class_info 類型常量,表示這個 Class 文件所定義的 類的直接父類。當前類的直接父類,以及它所有間接父類的 access_flag 中都不能有 ACC_FINAL 標記。對於介面來說,它的 Class 文件的 super_class 項的值必須是 對 constant_pool 表中項目的一個有效索引值。constant_pool 表在這個索引處的 項必須為代表 java.lang.Object 的 CONSTANT_Class_info 類型常量。 如果 Class 文件的 super_class 的值為 0,那這個 Class 文件只可能是定義的是 java.lang.Object 類,只有它是唯一沒有父類的類。這裡的父類索引是 00 06 表示其指向了常量池中第 6 個常量,通過我們之前的分析,我們知道第 6 個常量其最終的信息是 Object 類。因為其並沒有繼承任何類,所以 Demo 類的父類就是預設的 Object 類。interfaces_count 介面計數器,
interfaces_count 的值表示當前類或介面的直接父介面數量。
5.3 interfaces[] 介面表
interfaces[] 數組中的每個成員的值必須是一個對 constant_pool 表中項 目的一個有效索引值,它的長度為 interfaces_count。每個成員 interfaces[i] 必 須為CONSTANT_Class_info類型常量,其中0 ≤ i < interfaces_count。在 interfaces[]數組中,成員所表示的介面順序和對應的源 代碼中給定的介面順序(從左至右)一樣,即 interfaces[0]對應的是源代碼中最左 邊的介面。
這裡 Log 類的位元組碼文件中,因為並沒有實現任何介面,所以緊跟著父類索引後的兩個位元組是0x0000,這表示該類沒有實現任何介面。因此後面的介面索引表為空。
6. 欄位表集合
欄位表集合用於描述介面或者類中聲明的變數,這裡的數據為:00 00。
6.1 fields_count 欄位計數器
fields_count 的值表示當前 Class 文件 fields[]數組的成員個數。 fields[]數組中每一項都是一個 field_info 結構的數據項,它用於表示該類或介面聲明的類欄位或者實例欄位。
6.2 fields[] 欄位表
fields[]數組中的每個成員都必須是一個 fields_info 結構的數 據項,用於表示當前類或介面中某個欄位的完整描述。fields[]數組描述當前類或介面 聲明的所有欄位,但不包括從父類或父介面繼承的部分。
field_info 結構格式如下:field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
7. 屬性集合
7.1 attributes_count 屬性計數器
attributes_count 的值表示當前 Class 文件 attributes 表的成員個 數。attributes 表中每一項都是一個 attribute_info 結構的數據項。
7.2 attributes[]屬性表
屬性表,attributes 表的每個項的值必須是 attribute_info 結構(§4.7)。
屬性(Attributes)在 Class 文件格式中的 ClassFile 結構、field_info 結構,method_info 結構和 Code_attribute 結構都有使用,所有屬性的通用格式如下:
對於任意屬性,attribute_name_index 必須是對當前 Class 文件的常量池的有效 16 位 無符號索引。常量池在該索引處的項必須是 CONSTANT_Utf8_info 結構,表示當前屬性的名字。attribute_length 項的值給出了跟隨其後的位元組的長度,這個長度不包括 attribute_name_index 和 attribute_name_index 項的 6 個位元組。
有些屬性因 Class 文件格式規範所需,已被預先定義好。這些屬性在表 4.6 中列出,同時,被列出的信息還包括它們首次出現的Class文件版本和 Java SE 版本號。在本規範定義的環境 中,也就是已包含這些預定義屬性的 Class 文件中,它們的屬性名稱被保留,不能再被屬性表中其他的自定義屬性所使用。 下表是 class 文件的屬性:
8. 方法表集合
在欄位表後的 2 個位元組是一個方法計數器,表示類中總有有幾個方法,在欄位計數器後,才是具體的方法數據。這裡數據為:00 02 。
8.1 methods_count 方法計數器
methods_count 的值表示當前 Class 文件 methods[] 數組的成員個數。 Methods[] 數組中每一項都是一個 method_info 結構的數據項。8.2 methods[] 方法表
methods[] 數組中的每個成員都必須是一個 method_info 結構的 數據項,用於表示當前類或介面中某個方法的完整描述。如果某個 method_info 結構 的 access_flags 項既沒有設置 ACC_NATIVE 標誌也沒有設置 ACC_ABSTRACT 標誌, 那麼它所對應的方法體就應當可以被 Java 虛擬機直接從當前類載入,而不需要引用其它 類。method_info 結構可以表示類和介面中定義的所有方法,包括實例方法、類方法、 實例初始化方法方法和類或介面初始化方法方法。methods[]數組 只描述當前類或介面中聲明的方法,不包括從父類或父介面繼承的方法。 method_info 結構格式如下:method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
Log 類的位元組碼文件中,方法計數器的值為 00 02,表示一共有 2 個方法。
第一個方法:
00 0100 0700 0800 0100 0900 0000 1d00 0100 0100 0000 052a b700 01b1 0000 0001 000a 0000 0006 0001 0000 0003
根據前面的結構格式可以知道,
方法計數器後 2 個位元組表示方法訪問標識,這裡是 00 01,表示其實 ACC_PUBLIC 標識,對比上面的圖表可知其表示 public 訪問標識。
緊接著 2 個位元組表示方法名稱的索引,這裡是 00 07 表示指向了常量池第 7 個常量,查閱可知其指向了<init>
。
緊接著的 2 個位元組表示方法描述符索引項,這裡是 00 08 表示指向了常量池第 8 個常量,查閱可知其指向了()V
。
緊接著 2 個位元組表示屬性表計數器,這裡是 00 01 表示該方法的屬性表一共有 1 個屬性。屬性表的表結構如下:
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
前兩個位元組是名字索引、接著 4 個位元組是屬性長度、接著是屬性的值。這裡前兩個位元組為 0009,指向了常量池第 9 個常量,查詢可知其值為 Code,說明此屬性是方法的位元組碼描述。
Code 屬性是一個變長屬性,位於 method_info 結構的屬性表。一個 Code 屬性只為唯一一個方法、實例類初始化方法或類初始化方法保存 Java 虛擬機指令及相關輔助信息。 所有 Java 虛擬機實現都必須能夠識別 Code 屬性。如果方法被聲明為 native 或者 abstract 類型,那麼對應的 method_info 結構不能有明確的 Code 屬性,其它情況下, method_info 有必須有明確的 Code 屬性。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 屬性對應表結構知道,前 2 個位元組為 0009,即常量池第 9 個常量,查詢知道是字元串常量 Code
。
接著 4 個位元組表示屬性長度,這裡值為 0000 001d,即 29 的長度。下麵我們繼續分析 Code 屬性的數據內容。
緊接著 2 個位元組為 max_stack 屬性。這裡數據為 00 01,表示操作數棧深度的最大值。
緊接著 2 個位元組為 max_locals 屬性。這裡是數據為 00 01,表示局部變數表所需的存儲空間為 1 個 Slot。在這裡 max_locals 的單位是 Slot,Slot 是虛擬機為局部變數分配記憶體所使用的最小單位。
接著 4 個位元組為 code_length,表示生成位元組碼這裡給的長度。這裡數據為 00 00 00 05,表示生成位元組碼長度為 5 個位元組。那麼緊接著 5 個自己就是對應的數據,這裡數據為 2a b7 00 01 b1,這一串數據其實就是位元組碼指令。通過查詢位元組碼指令表,可知其對應的位元組碼指令:
-
讀入 2A,查表得 0x2A 對應的指令為 aload_0,這個指令的含義是將第 0 個 Slot 中為 reference 類型的本地變數推送到操作數棧頂。
-
讀入 B7,查表得0xB7對應的指令為 invokespecial,這條指令的作用是以棧頂的 reference 類型的數據所指向的對象作為方法接收者,調用此對象的實例構造器方法、private 方法或者它的父類的方法。這個方法有一個 u2 類型的參數說明具體調用哪一個方法,它指向常量池中的一個 CONSTANT_Methodref_info 類型常量,即此方法的方法符號引用。
-
讀入 00 01,這是 invokespecial 的參數,查常量池得 0x0001 對應的常量為實例構造器“”方法的符號引用。
-
讀入 B1,查表得0xB1對應的指令為 return,含義是返回此方法,並且返回值為void。這條指令執行後,當前方法結束。
接著 2 個位元組為異常表長度,這裡數據為 00 00,表示沒有異常表數據。那麼接下來也就不會有異常表的值。
緊接著 2 個位元組是屬性表的長度,這裡數據為 00 01,表示有一個屬性。該屬性長度為一個 attribute_info 那麼長。
首先,前兩個位元組表示屬性名稱索引,這裡數據為:00 0A。指向了第 10 個常量,查閱可知值為:LineNumberTable。LineNumberTable 屬性是可選變長屬性,位於 Code(§4.7.3)結構的屬性表。它被調試 器用於確定源文件中行號表示的內容在 Java 虛擬機的 code[]數組中對應的部分。在 Code 屬性 的屬性表中,LineNumberTable 屬性可以按照任意順序出現,此外,多個 LineNumberTable 屬性可以共同表示一個行號在源文件中表示的內容,即 LineNumberTable 屬性不需要與源文件 的行一一對應。
LineNumberTable 屬性格式如下:
LineNumberTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{
u2 start_pc;
u2 line_number;
} line_number_table[line_number_table_length];
}
其前兩個位元組是屬性名稱索引,就是上面已經分析過的 00 0A。
接著 4 個位元組是屬性長度,這裡數據為 00 00 00 06,表示有 6 個位元組的數據。接著 2 個位元組是 LineNumberTable 的長度,這裡數據是 00 01,表示長度為 1。接著跟著 1 個 line_number_info 類型的數據,下麵是 line_number_info 表的結構,其包含了 start_pc 和 line_number 兩個 u2 類型的數據項。前者是位元組碼行號,後者是 Java 源碼行號。
那麼接下來 2 個位元組為 00 00,即 start_pc 表示的位元組碼行號為第 0 行。接著 00 03,即 line_number 表示 Java 源碼行號為第 3 行。
從上面的反編譯來看,上面的分析也是對的:
{ public com.hello.test.Log(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String hello world! 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 5: 0 line 6: 8 }
第二個方法這裡就不詳細分析了,大家可以自己對著上面的反編譯結果進行分析。
第二個方法開頭是 0009,表示的是 ACC_PUBLIC 與 ACC_STATIC 合在一起的結果。
總結
到這裡我們通過對 Log 類的解析,從而對 Java 類文件結構有了一個全面的認識。進一步還簡單瞭解了 Java 虛擬機以及 Java 虛擬機規範。希望讀完這篇文章,大家能對 Java 類文件結構有一個深入的認識。 最後用一張圖來總結一下: