1. 程式人生 > >JVM系列第5講:位元組碼檔案結構

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 類檔案結構有一個深入的認識。

鄭州不孕不育檢查

鄭州不孕不育

鄭州最好的不孕不育醫院

鄭州不孕不育