JVM基礎系列第5講:字節碼文件結構


溫馨提示:此篇文章長達兩萬字,圖片50多張,內容非常多,建議收藏后再看。

前面我們說到 Java 虛擬機使用字節碼實現了跨平台的願景,無論什么系統,我們都可以使用 Java 虛擬機解釋執行字節碼文件。但其實字節碼是有一套規范的,而規定字節碼格式的就是《Java 虛擬機規范》。《Java 虛擬機規范》規定了 Java 虛擬機結構、Class 類文件結構、字節碼指令等內容。其中類文件結構是有必要了解的一個內容。

字節碼文件結構是一組以 8 位字節為基礎的二進制流,各數據項目嚴格按照順序緊湊地排列在 Class 文件之中,中間沒有添加任何分隔符。在字節碼結構中,有兩種最基本的數據類型來表示字節碼文件格式,分別是:無符號數和表。

無符號數屬於最基本的數據類型。它以 u1、u2、u4、u8 六七分別代表 1 個字節、2 個字節、4 個字節、8 個字節的無符號數。無符號數可以用來描述數字、索引引用、數量值或者按照 UTF-8 編碼構成的字符串值。例如下表中第一行中的 u4 表示 Class 文件前 4 個字節表示該文件的魔數,第二行的 u2 表示該 Class 文件第 5-6 個字節表示該 JDK 的次版本號。

表是由多個無符號數或者其他表作為數據項構成的復合數據類型。所有表都習慣性地以_info結尾。表用於描述有層次關系的復合結構的數據,例如下表第 5 行表示其實一個類型為 cp_info 的表(常量池),這里面存儲了該類的所有常量。

而整個字節碼文件本質上就是一張表,它由下面幾個部分組成:

為了便於理解,我將一個完整的表划分為以下七個部分,這七個部分組成了一個完整的 Class 字節碼文件:

  • 魔數與Class文件版本
  • 常量池
  • 訪問標志
  • 類索引、父類索引、接口索引
  • 字段表集合
  • 方法表集合
  • 屬性表集合

在開始之前,我們先寫一個最簡單的入門 Hello World。接下來我們將以這個 Hello World 文件編譯后的字節碼文件為例子,來解析字節碼文件內容。

public class Demo{
	public static void main(String args[]){
		System.out.println("Hello World.");
  }
}

接着在命令行運行javac Demo.java命令編譯這個類,這時會生成一個 Demo.class 文件。

接着我們用純文本編輯器打開生成的 Demo.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 0009 4465 6d6f 2e6a
6176 610c 0007 0008 0700 170c 0018 0019
0100 0b48 656c 6c6f 2057 6f72 6c64 0700
1a0c 001b 001c 0100 0444 656d 6f01 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 0001
0009 000b 000c 0001 0009 0000 0025 0002
0001 0000 0009 b200 0212 03b6 0004 b100
0000 0100 0a00 0000 0a00 0200 0000 0300
0800 0400 0100 0d00 0000 0200 0e

首先我們要清楚,字節碼文件是使用十六進制進行編碼的,而十六進制使用0x表示。接下來我們用上面「Hello World」的字節碼文件為例子,一步步分析這七部分內容。

魔數與Class文件版本

Class 文件的第 1 - 4 個字節代表了該文件的魔數(Magic Number)。它唯一的作用是確定這個文件是否為一個能被虛擬機接受的 Class 文件,其值固定是:0xCAFEBABE(咖啡寶貝)。如果一個 Class 文件的魔數不是 0xCAFEBABE,那么虛擬機將拒絕運行這個文件。

Class 文件的第 5 - 6 個字節代表了 Class 文件的次版本號(Minor Version),即編譯該 Class 文件的 JDK 次版本號。

Class 文件的第 7 - 8 個字節代表了 Class 文件的主版本號(Major Version),即編譯該 Class 文件的 JDK 主版本號。

高版本的 JDK 能向下兼容以前笨笨的 Class 文件,但不能運行新版本的 Class 文件。例如一個 Class 文件是使用 JDK 1.5 編譯的,那么我們可以用 JDK 1.7 虛擬機運行它,但不能用 JDK 1.4 虛擬機運行它。下表列出了各個版本 JDK 的十六進制版本號信息:

我們看看之前的 Demo 文件的 Class 文件內容,其前 8 個字節分別是:cafe babe 0000 0034

對比上面表格中的數據,那么我們可以知道,這個 Class 文件是由 JDK1.8 編譯的。

常量池

緊跟版本信息之后的是常量池信息,其中前 2 個字節表示常量池個數,其后的不定長數據則表示常量池的具體信息。

我們可以從上圖知道,常量池的常量都是由cp_info這種表結構組成的,而且表結構不同其大小也不同。在 Java 虛擬機規范中一共有 14 種 cp_info 類型的表結構。

而上面這些 cp_info 表結構又有不同的數據結構,其對應的數據結構如下圖所示。

cp_info表結構一共有三個字段,第一個字段表示這個表結構的標示值,有一個字節大小,對應我們上一個表格中的數字。第二、三個字段表示其表結構的描述,不同字段其意思不太一樣。

看到這里可能有點犯模糊,這么些表格到底應該怎么用呢?沒關系,我們舉個例子就清楚了。

接下來我們繼續看看 Hello World 字節碼文件的內容。上一小節說到字節碼文件的版本,那么接下來就是常量池的內容了。

Hello World 文件字節碼對應的內容是:00 1d,其值為 29,表示一共有 29 - 1 = 28 個常量。

緊跟着常量池的就是 28 個常量了,因為每個常量都對應不同的類型,所以我們無法得知其具體大小,只能一個個分析。

第 1 個常量。緊接着 001d 的后一個字節為 0A,為十進制數字 10,查表可知其為方法引用類型(CONSTANT_Methodref_info)的常量。

再查 cp_info 對應的表結構知道,該常量項第 2 - 3 個字節表示類信息,第 4 - 5 個字節表示名稱及類描述符。

接下來我們取出這部分的數據:0a 0600 000f

該常量項第 2 - 3 個字節,其值為 00 06,表示指向常量池第 6 個常量所表示的信息。根據后面我們分析的結果知道第 6 個常量是 java/lang/Object 。第 4 - 5 個字節,其值為 000f,表示指向常量池第 15 個常量所表示的信息,根據 javap 反編譯出來的信息可知第 10 個常量是 <init>:()V。將這兩者組合起來就是:java/lang/Object.<init>:V,即 Object 的 init 初始化方法。

大致就是按照上面的方式去分析每一個常量的值和意義,接下來我繼續分析接下來的 27 個常量。

第 2 個常量,數據為 09 0010 0011。緊接着 000f 的后一個字節為 09,表示該常量為字段的符號引用(CONSTANT_Fieldref_info)。從上面的總表查閱知道,該常量項第 2 - 3 個字節表示類信息,這里是 0010 表示指向常量池第 16 個常量所表示的信息,根據 javap 反編譯我們知道其是 java/lang/System.out。該常量項的第 4 - 5 個字節表示名稱及類描述符,這里值為 0011 表示指向常量池第 17 個常量所表示的信息,javap 反編譯得知是 Ljava/io/PrintStream。結合起來就是:java/lang/System.out:Ljava/io/PrintStream;

第 3 個常量,數據為 08 00 12。緊接着 0011 的后一個字節為 08,表示該常量為字符串引用類型(CONSTANT_String_info)的常量。從上面的總表查閱知道,該常量項第 2 - 3 個字節表示指向字符串字面量的索引,這里是 0012 表示指向常量池的第 18 個常量。javap 反編譯得知其是一個Hello World!字符串。

第 4 個常量,數據為 0A 0013 0014。緊接着 0012 的后一個字節為 0A,表示該常量為方法引用類型(CONSTANT_MethodHandle_info)的常量。從上面的總表查閱知道,該常量項第 2 - 3 個字節表示類信息,這里是 0013 表示指向常量池第 19 個常量所表示的信息。該常量項的第 4 - 5 個字節表示名稱及類描述符,這里值為 0014 表示指向常量池第 20 個常量所表示的信息。結果是:java/io/PrintStream.println:(Ljava/lang/String;)V

第 5 個常量,數據為 07 00 15。緊跟着 0014 后的是 07,表示是類信息類型常量,表結構如下。該表后緊跟着一個 2 個字節的索引,這里是 0015,其指向了常量池第 21 個常量,反編譯得知其值為Demo

第 6 個常量,數據為 07 0016。07 表示其是類信息類型常量,其指向了常量池第 22 個常量。從后邊的分析可以知道,第 22 個常量為字符串java/lang/Object

第 7 個常量,數據為 01 0006 3C 69 6E 69 74 3E。其中 01 表示其是字符串(CONSTANT_Utf8_info)的常量,0006 表示其字符串長度為 6 個字節。隨后跟着的 3C 69 6E 69 74 3E 為字符串的值。在 Class 文件中,字符串是使用 ASCII 碼進行編碼的,我們將這些十六進制字符轉換成對應的 ASCII 碼之后,其值為:<init>

第 8 個常量,數據為 01 00 03 28 29 56。其中 01 表示其是字符串(CONSTANT_Utf8_info)的常量,0003 表示其字符串長度為 3 個字節。隨后跟着的 28 29 56 為字符串的值。在 Class 文件中,字符串是使用 ASCII 碼進行編碼的,我們將這些十六進制字符轉換成對應的 ASCII 碼之后,其值為:()V

第 9 個常量,數據為 01 00 04 43 6f 64 65。它是一個字符串常量,轉換之后是:Code

第 10 個常量,數據位 01 00 0f 4c 696e 654e 756d 6265 7254 6162 6c65 是一個字符串常量,轉換之后是:LineNumberTable

第 11 個常量,數據為 01 00 04 6d 6169 6e。它是一個字符串常量,轉換之后是:main

第 12 個常量,數據為 01 0016 285b 4c6a 6176 612f 6c61 6e67 2f53 7472 696e 673b 2956 是一個字符串常量,轉換之后是:([Ljava/lang/String;)V

第 13 個常量,數據為 01 00 0a 53 6f75 7263 6546 696c 65。它是一個字符串常量,轉換之后是:SourceFile

第 14 個常量,數據為 01 0009 4465 6d6f 2e6a 6176 61。它是一個字符串常量,轉換之后是:Demo.java

第 15 個常量,數據為 0c 0007 0008。這里表示 tag 的值是 0C,表示該常量為方法引用類型(CONSTANT_NameAndType_info)的常量。從上面的總表查閱知道,該常量項第 2 - 3 個字節表示字段或方法名的索引,這里是 0007 表示指向常量池第 7 個常量所表示的信息,即<init>。該常量項的第 4 - 5 個字節表示字段或方法描述符的索引,這里值為 0008 表示指向常量池第 8 個常量所表示的信息,即()V。所以第 15 個常量表示的信息其實是:"<init>":()V

第 16 個常量,數據為 07 00 17。這里表示 tag 的值是 07,表示該常量為類信息類型(CONSTANT_Class_info)的常量。從上面的總表查閱知道,該常量項第 2 - 3 個字節表示全限定名常量項的索引,這里是 0017 表示指向常量池第 23 個常量所表示的信息,即 java/lang/System

第 17 個常量,數據為 0c 0018 0019。這里表示 tag 的值是 0C,表示該常量為方法引用類型(CONSTANT_NameAndType_info)的常量。從上面的總表查閱知道,該常量項第 2 - 3 個字節表示字段或方法名的索引,這里是 0018 表示指向常量池第 24 個常量所表示的信息,即 out。該常量項的第 4 - 5 個字節表示字段或方法描述符的索引,這里值為 0019 表示指向常量池第 25 個常量所表示的信息,即Ljava/io/PrintStream;。所以第 17 個常量表示的信息其實是:out:Ljava/io/PrintStream;

第 18 個常量,數據為 01 00 0b 48 656c 6c6f 2057 6f72 6c64 。它是一個字符串常量,轉換之后是:Hello World

第 19 個常量,數據為 07 001a。這里表示 tag 的值是 07,表示該常量為類信息類型(CONSTANT_Class_info)的常量。從上面的總表查閱知道,該常量項第 2 - 3 個字節表示全限定名常量項的索引,這里是 001A 表示指向常量池第 26 個常量所表示的信息,即java/io/PrintStream

第 20 個常量,數據為 0c 001b 001c 。這里表示 tag 的值是 0C,表示該常量為方法引用類型(CONSTANT_NameAndType_info)的常量。從上面的總表查閱知道,該常量項第 2 - 3 個字節表示字段或方法名的索引,這里是 001B 表示指向常量池第 27 個常量所表示的信息,即println。該常量項的第 4 - 5 個字節表示字段或方法描述符的索引,這里值為 001C 表示指向常量池第 28 個常量所表示的信息,即(Ljava/lang/String;)V。所以這里第 20 個常量的值為 println:(Ljava/lang/String;)V

第 21 個常量,數據為 01 00 04 44 656d 6f。是一個字符串常量,轉換之后是:Demo

第 22 個常量,數據為 01 0010 6a61 7661 2f6c 616e 672f 4f62 6a65 6374。是一個字符串常量,轉換之后是:java/lang/Object

第 23 個常量,數據為 01 00 10 6a 6176 612f 6c61 6e67 2f53 7973 7465 6d。是一個字符串常量,轉換之后是:java/lang/System

第 24 個常量,數據為 01 0003 6f75 74。是一個字符串常量,轉換之后是:out

第 25 個常量,數據為 01 0015 4c6a 6176 612f 696f 2f50 7269 6e74 5374 7265 616d 3b。是一個字符串常量,轉換之后是:Ljava/io/PrintStream;

第 26 個常量,數據為 01 0013 6a61 7661 2f69 6f2f 5072 696e 7453 7472 6561 6d。是一個字符串常量,轉換之后是:java/io/PrintStream

第 27 個常量,數據為 01 0007 7072 696e 746c 6e。是一個字符串常量,轉換之后是:println

第 28 個常量,數據為 01 0015 284c 6a61 7661 2f6c 616e 672f 5374 7269 6e67 3b29 56。是一個字符串常量,轉換之后是:(Ljava/lang/String;)V

到這里,我們常量池里 28 個常量已經全部解析完了。我們通過手動分析,了解了常量池的構成,但很多時候我們可以借助 JDK 提供的 javap 命令直接查看 Class 文件的常量池信息。

當我們運行javap -verbose Demo.class時,控制台會打印出該 Class 文件的構成信息,其中就包括了常量池的信息。

將利用 javap 打印出的結果,與我們手動分析的結果對比一下,你會發現結果是一致的。

訪問標志

在常量池結束之后,緊接着的兩個字節代表類或接口的訪問標記(access_flags)。這里的數據為 00 21。

這個標志用於識別一些類或者接口層次的訪問信息,包括:這個Class是類還是接口、是否定義為public類型、是否定義為abstract類型等。具體的標志位以及標志的含義見下表。

在這里這兩個字節是 00 21,通過查看我們並沒有發現有標志值是 00 21 的標志名稱。這是因為這里的訪問標志可能是由多個標志名稱組成的,所以字節碼文件中的標志值其實是多個值進行或運算的結果。

通過查閱上述表格,我們可以知道,00 21 由 00 01(第1行)和 00 20(第3行)進行或運算得來。也就是說該類的訪問標志是 public 並且允許使用 invokespecial 字節碼指令的新語義。

類索引、父類索引、接口索引

在訪問標記后,則是類索引、父類索引、接口索引的數據,這里數據為:00 05 00 06 00 00。

類索引和父類索引都是一個u2類型的數據,而接口索引集合是一組u2類型的數據的集合,Class 文件中由這三項數據來確定這個類的繼承關系。

類索引。類索引用於確定這個類的全限定名,它用一個 u2 類型的數據表示。這里的類索引是 00 05 表示其指向了常量池中第 5 個常量,通過我們之前的分析,我們知道第 5 個常量其最終的信息是 Demo 類。

父類索引。父類索引用於確定這個類的父類的全限定名,父類索引用一個u2類型的數據表示。這里的父類索引是 00 06 表示其指向了常量池中第 6 個常量,通過我們之前的分析,我們知道第 6 個常量其最終的信息是 Object 類。因為其並沒有繼承任何類,所以 Demo 類的父類就是默認的 Object 類。

接口索引。接口索引集合就用來描述哪個類實現了哪些接口,這些被實現的接口將按 implements 語句(如果這個類本身就是一個接口,則應當是extends語句)后的接口順序從左到右排列在接口索引集合中。對於接口索引集合,入口第一項是 u2 類型的數據為接口計數器(interfaces_count),表示索引表的容量,而在接口計數器后則緊跟着所有的接口信息。如果該類沒有實現任何接口,則該計數器值為0,后面接口的索引表不再占用任何字節。

這里 Demo 類的字節碼文件中,因為並沒有實現任何接口,所以緊跟着父類索引后的兩個字節是0x0000,這表示該類沒有實現任何接口。因此后面的接口索引表為空。

字段表集合

字段表集合用於描述接口或者類中聲明的變量,這里的數據為:00 00。

這里說的字段包括類級變量和實例級變量,但不包括在方法內部聲明的局部變量。在類接口集合后的2個字節是一個字段計數器,表示總有有幾個屬性字段。在字段計數器后,才是具體的屬性數據。

字段表的每個字段用一個名為 field_info 的表來表示,field_info 表的數據結構如下所示:

因為我們並沒有聲明任何的類成員變量或類變量,所以在 Demo 的字節碼文件中,字段計數器為 00 00,表示沒有屬性字段。

方法表集合

在字段表后的 2 個字節是一個方法計數器,表示類中總有有幾個方法,在字段計數器后,才是具體的方法數據。這里數據為:00 02 。

方法表中的每個方法都用一個 method_info 表示,其數據結構如下:

Demo 類的字節碼文件中,方法計數器的值為 00 02,表示一共有 2 個方法。

第 1 個方法,這里數據為:00 01 00 07 00 08 00 01 00 09 00 0000 1d 00 01 00 01 00 0000 05 2a b7 00 01 b1 0000 0001 000a 0000 0006 0001 0000 0001。方法計數器后 2 個字節表示方法訪問標識,這里是 00 01,表示其實 ACC_PUBLIC 標識,對比上面的圖表可知其表示 public 訪問標識。緊接着 2 個字節表示方法名稱的索引,這里是 00 07 表示指向了常量池第 7 個常量,查閱可知其指向了<init>。緊接着的 2 個字節表示方法描述符索引項,這里是 00 08 表示指向了常量池第 8 個常量,查閱可知其指向了()V

緊接着 2 個字節表示屬性表計數器,這里是 00 01 表示該方法的屬性表一共有 1 個屬性。屬性表的表結構如下:

前兩個字節是名字索引、接着 4 個字節是屬性長度、接着是屬性的值。這里前兩個字節為 0009,指向了常量池第9個常量,查詢可知其值為Code,說明此屬性是方法的字節碼描述。 Code 屬性的表結構如下:

根據 Code 屬性對應表結構知道,前 2 個字節為 0009,即常量池第 9 個常量,查詢知道是字符串常量Code。接着 4 個字節表示屬性長度,這里值為 1D,即 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 那么長。attribute_info 屬性表的表結構如下。

首先,前兩個字節表示屬性名稱索引,這里數據為:00 0A。指向了第 10 個常量,查閱可知值為:LineNumberTable。LineNumberTable 表的表結構如下圖所示。

其前兩個字節是屬性名稱索引,就是上面已經分析過的 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 01,即 line_number 表示 Java 源碼行號為第 1 行。

到此,我們方法表集合的第一個方法分析結束。我們通過 javap 反編譯查看,可以看到 Code 和 LineNumberTable 都是完全正確的。

接下來分析第 2 個方法。第二個方法的數據為:TODO。

前 2 個字節為方法訪問標識,這里數據為 00 09,標識方法標識符為 public static void。

接着 2 個字節為方法名稱索引項,這里數據為 00 0b,即常量池第 11 個常量,查詢可知其值是main

接着 2 個字節為方法描述符索引項,這里數據為 00 0c,即常量池第 12 個常量,查詢可知其值是([Ljava/lang/String;)V

接着 2 個常量標識屬性表的數量,這里數據為 00 01,表示后面有 1 個類型為 表結構為 attribute_info 的屬性信息。attribute_info 表的表結構如下。

即緊接着 2 個字符表示屬性名的索引項,這里數據為 00 09,即對應常量池第 9 個常量,查詢可知其值為:Code。Code 屬性的表結構如下圖所示。

Code 屬性前 2 個字節表示其名字,這里分析過了,是Code

接着 4 個字節表示屬性的長度,這里數據是 00 00 00 25,表示長度為 37。

緊接着 2 個字節為 max_stack 屬性。這里數據為 00 02,表示操作數棧深度的最大值為 2,其實是說有兩個局部變量。

緊接着 2 個字節為 max_locals屬性。這里是數據為 00 01,表示局部變量表所需的存儲空間為 1 個 Slot。在這里 max_locals的單位是Slot,Slot是虛擬機為局部變量分配內存所使用的最小單位。

接着 4 個字節為 code_length,表示生成字節碼這里給的長度。這里數據為 00 00 00 09,表示生成字節碼長度為 9 個字節。那么緊接着 9 個自己就是對應的數據,這里數據為 b2 00 02 12 03 b6 00 04 b1,這一串數據其實就是字節碼指令。通過查詢字節碼指令表,可知其對應的字節碼指令。上面分析過一次了,這里就不再分析了。

接着 2 個字節為異常表長度,這里數據為 00 00,表示沒有異常表數據。那么接下來也就不會有異常表的值。

緊接着 2 個字節是屬性表的長度,這里數據為 00 01,表示有一個屬性。該屬性長度為一個 attribute_info 那么長。attribute_info 屬性表的表結構如下。

首先,前兩個字節表示屬性名稱索引,這里數據為:00 0A。指向了第 10 個常量,查閱可知值為:LineNumberTable。LineNumberTable 表的表結構如下圖所示。

其前兩個字節是屬性名稱索引,就是上面已經分析過的 00 0A。

接着 4 個字節是屬性長度,這里數據為 00 00 00 0A,表示有 10 個字節的數據。接着 2 個字節是 LineNumberTable 的長度,這里數據是 00 02,表示長度為 2。接着跟着 2 個 line_number_info 類型的數據,下面是 line_number_info 表的結構,其包含了 start_pc 和 line_number 兩個 u2 類型的數據項。前者是字節碼行號,后者是 Java 源碼行號。

第 1 個 line_number_info,即接下來 2 個字節為 00 00,即 start_pc 表示的字節碼行號為第 0 行。接着 00 03,即 line_number 表示 Java 源碼行號為第 3 行。

第 2 個 line_number_info,即接下來 2 個字節為 00 08,即 start_pc 表示的字節碼行號為第 8 行。接着 00 04,即 line_number 表示 Java 源碼行號為第 4 行。

這里的每個 line_number_info 占用 4 個字節,兩個 line_number_info 一共 8 個字節。再加上表示 line_number_info 數量的 2 個字節,一共 10 個字節。剛好就與 attribute_length 的 00 00 00 0A 數據吻合。

到這里,第 2 個方法也分析結束了。同樣我們通過javap命令反編譯看看,會發現反編譯的結果與我們分析的完全吻合,這說明我們的分析是正確的。

屬性表集合

這里或許有人會迷惑,上面我們不是分析過屬性表了么。其實上面分析的是方法中的屬性,而這個是類中的屬性。這個就像局部變量和類成員變量一樣,是不同的。

緊接着我們剩下的數據為:00 0100 0d00 0000 0200 0e,這些就是屬性表集合的數據了。

根據上面的表格我們知道,緊跟着的 2 個字節數據是屬性表屬性數量,這里數據為 00 01,表示有 1 個屬性。后面緊跟着 1 個表結構為 attribute_info 的屬性數據。attribute_info 表的結構如下圖所示。

前兩個字段為屬性名稱索引,這里數據為 00 0d,表示第 13 個常量池,查詢可知這里的值是:SourceFile。SourceFile 屬性的表結構如下圖所示。

SourceFile 表結構前兩個字節我們已經分析過,數據為 00 0d,表示第 13 個常量池,指的是SourceFile這個值。接着我們看后面 4 個字節,這里數據為 00 00 00 02,表示屬性長度為 2 個字節。緊跟着的 2 個字節表示 SourceFile 的常量池索引,即該字節碼文件的源文件名稱,這里數據是 00 0e,即常量池的第 14 項,即Demo.java。所以這個屬性項標識了該字節碼文件的源文件名稱為 Demo.java。

我們通過 javap 反編譯一下,可以發現與我們的分析完全一致。

到這里,我們就從頭到尾將字節碼文件的每個字節的數據分析完畢。通過這么一次分析,相信大家對於字節碼文件的構成已經了然於胸了。這樣的分析非常耗費時間,但是確實對字節碼結構最好的一次學習。有時候最笨的方法,恰恰是最高效的方法。還沒堅持下來的同學,要至少堅持獨立分析一次,這樣的收獲是很大的。

但在實際使用或分析問題的時候,我們通常用 javap 工具幫助我們完成這個過程,這樣能提高效率。使用 javap 工具很簡單,只需要這樣使用:javap -verbose Demo.class 就可以將字節碼文件全部分析出來。下面給出此次 Demo.class 文件的反編譯完整截圖。

總結

到這里我們通過對 Hello World 的解析,從而對 Java 類文件結構有了一個全面的認識。進一步還簡單了解了 Java 虛擬機以及 Java 虛擬機規范。希望讀完這篇文章,大家能對 Java 類文件結構有一個深入的認識。 最后用一張圖來總結一下:


如果只是看,其實無法真正學會知識的。為了幫助大家更好地學習,我建了一個虛擬機群,專門討論學習 Java 虛擬機方面的內容,每周針對我所發文章進行討論答疑。如果你有興趣,關注「陳樹義」公眾號,通過右下角菜單「入群交流」加我好友,小助手會拉你入群。


JVM基礎系列文章目錄


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM