從 HelloWorld 看 Java 字節碼文件結構


很多時候,我們都是從代碼層面去學習如何編程,卻很少去看看一個個 Java 代碼背后到底是什么。今天就讓我們從一個最簡單的 Hello World 開始看一看 Java 的類文件結構。

在開始之前,我們先寫一個最簡單的入門 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

可以看到我們簡單的 5 行代碼到最后就濃縮成了上面那一長串數字字母組成的十六進制符號。而當我們運行該 Java 類時,控制台能准確地輸出「Hello World」,所以們可以斷定這一長串的符號必定遵守着某種規則,而這個規則其實就是:Java虛擬機規范

Java虛擬機規范

Java 虛擬機規范中規定了 Java 虛擬機結構、Class 類文件結構、字節碼指令等內容,其中對於軟件開發人員來說,類文件結構是有必要了解的一個內容。

Java 虛擬機的類文件結構是一組以 8 位字節為基礎的二進制流,各數據項目嚴格按照順序緊湊地排列在 Class 文件之中,中間沒有添加任何分隔符,這使得整個 Class 文件中存儲的內容幾乎全都是程序需要的數據,沒有空隙存在。

如果你對 Java 虛擬機規范有興趣,可以參考 Github 上的《Java 虛擬機規范》

Java 虛擬機

說完了 Java 虛擬機規范,就需要了解一下 Java 虛擬機這個概念。

其實 Java 虛擬機就是一個虛擬的計算機。與真實的計算機一樣,Java 虛擬機有自己完善的硬件體系,如處理器、堆棧、寄存器,還有相應的指令集系統。虛擬機與我們的電腦唯一的區別是:虛擬機的處理器、內存堆棧是用軟件虛擬出來的,而我們電腦的處理器和內存則是真真實實的。

雖然名字是叫 Java 虛擬機,但 Java 虛擬機與 Java 語言沒有直接關系,它只按照 Java 虛擬機規范去讀取 Class 文件,並按照規定去解析、執行字節碼指令,僅此而已。

如果你夠牛逼,你完全可以寫一個編譯器,將 C 語言代碼編譯成符合 Java 虛擬機規范的字節碼文件,那么 Java 虛擬機也是可以執行的。

准確地說,Java 虛擬機與字節碼文件(Class文件)綁定。

Java類文件結構

Java 虛擬機規范中定義了許多規范,其中有一部分定義了字節碼的結構和規范。Java 虛擬機規范定義了兩種數據類型來表示 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 字節碼文件:

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

接下來我們用上面「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 編譯的。

文章首發於【博客園-陳樹義】,點擊跳轉到原文《從 HelloWorld 看 Java 字節碼文件結構》

常量池

Class 文件的第 9 - 10 個字節用於表示常量池常量的個數(constant_pool_count),那么緊跟着就有 constant_pool_count - 1 個常量。我們Class 文件第 9 - 10 個字節為 001d,表示有 28 個常量。

每個常量池的常量都用一個類型為 cp_info 的表表示,該表有 14 個值,分別是:

第 1 個常量。緊接着 001d 的后一個字節為 0A,表示該常量為方法引用類型(CONSTANT_MethodHandle_info)的常量。從上面的總表查閱知道,該常量項第 2 - 3 個字節表示類信息,這里是 0006 表示指向常量池第 6 個常量所表示的信息。該常量項的第 4 - 5 個字節表示名稱及類描述符,這里值為 000f 表示指向常量池第 10 個常量所表示的信息。

第 2 個常量。緊接着 000f 的后一個字節為 09,表示該常量為字段引用類型(CONSTANT_Fieldref_info)的常量。從上面的總表查閱知道,該常量項第 2 - 3 個字節表示類信息,這里是 0010 表示指向常量池第 16 個常量所表示的信息。該常量項的第 4 - 5 個字節表示名稱及類描述符,這里值為 0011 表示指向常量池第 17 個常量所表示的信息。

第 3 個常量。緊接着 0011 的后一個字節為 08,表示該常量為字符串引用類型(CONSTANT_String_info)的常量。從上面的總表查閱知道,該常量項第 2 - 3 個字節表示指向字符串字面量的索引,這里是 0012 表示指向常量池的第 18 個常量。

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

第 5 個常量,是類信息類型常量,其指向了常量池第 21 個常量。

第 6 個常量,是類信息類型常量,其指向了常量池第 22 個常量。

第 7 個常量。這里表示 tag 的值是 01,表示該常量為一個字符串(CONSTANT_Utf8_info)的常量。從上面的總表查閱知道,該常量項第 2 - 3 個字節表示該字符串的長度,這里是 0006 表示該字符串長度為 6 個字節。這里緊接着 01 的六個字節為 3C 69 6E 69 74 3E。在 Class 文件中,字符串是使用 ASCII 碼進行編碼的,我們將這些十六進制字符轉換成對應的 ASCII 碼之后,其值為:<init>

第 8 個常量,是一個字符串常量,轉換之后是:()V

第 9 個常量,是一個字符串常量,轉換之后是:Code

第 10 個常量,是一個字符串常量,轉換之后是:LineNumberTable

第 11 個常量,是一個字符串常量,轉換之后是:main

第 12 個常量,是一個字符串常量,轉換之后是:([Ljava/lang/String;)V

第 13 個常量,是一個字符串常量,轉換之后是:SourceFile

第 14 個常量,是一個字符串常量,轉換之后是:Demo.java

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

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

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

第 18 個常量,是一個字符串常量,轉換之后是:Hello World

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

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

第 21 個常量,是一個字符串常量,轉換之后是:Demo

第 22 個常量,是一個字符串常量,轉換之后是:java/lang/Object

第 23 個常量,是一個字符串常量,轉換之后是:java/lang/System

第 24 個常量,是一個字符串常量,轉換之后是:out

第 25 個常量,是一個字符串常量,轉換之后是:Ljava/io/PrintStream;

第 26 個常量,是一個字符串常量,轉換之后是:java/io/PrintStream

第 27 個常量,是一個字符串常量,轉換之后是:println

第 28 個常量,是一個字符串常量,轉換之后是:(Ljava/lang/String;)V

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

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

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

訪問標志

在常量池結束之后,緊接着的兩個字節代表訪問標記(access_flags),這個標志用於識別一些類或者接口層次的訪問信息,包括:這個Class是類還是接口、是否定義為public類型、是否定義為abstract類型等。具體的標志位以及標志的含義見下表。

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

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

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

類索引和父類索引都是一個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,這表示該類沒有實現任何接口。因此后面的接口索引表為空。

字段表集合

字段表集合用於描述接口或者類中聲明的變量。這里說的字段包括類級變量和實例級變量,但不包括在方法內部聲明的局部變量。

在類接口集合后的2個字節是一個字段計數器,表示總有有幾個屬性字段。在字段計數器后,才是具體的屬性數據。字段表的每個字段用一個名為 field_info 的表來表示,field_info 表的數據結構如下所示:

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

方法表集合

在字段表后的 2 個字節是一個方法計數器,表示類中總有有幾個方法。在字段計數器后,才是具體的方法數據。方法表中的每個方法都用一個 method_info 表示,其數據結構如下:

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

第 1 個方法。方法計數器后 2 個字節表示方法訪問標識,這里是 00 01,表示其實 ACC_PUBLIC 標識,即該方法訪問表示為 public。緊接着 2 個字節表示方法名稱的索引,這里是 00 07 表示指向了常量池第 7 個常量,查閱可知其指向了<init>。緊接着的 2 個字節表示方法描述符索引項,這里是 00 08 表示指向了常量池第 8 個常量,查閱可知其指向了()V。緊接着 2 個字節表示屬性表計數器,這里是 00 01 表示該方法一共有 1 個屬性。緊接着的一連串就是屬性表的內容。

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

如果讀完覺得有收獲,可以點贊評論,讓我寫出更多的好文章。

文章首發於【博客園-陳樹義】,點擊跳轉到原文《從 HelloWorld 看 Java 字節碼文件結構》


免責聲明!

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



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