從 HelloWorld 看 Java 位元組碼檔案結構

來源:互聯網
上載者:User

從 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 虛擬機器

說完了 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檔案版本
  • 常量池
  • 訪問標誌
  • 類索引、父類索引、介面索引
  • 欄位表集合
  • 方法表集合
  • 屬性工作表集合
魔數與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 編譯的。

常量池

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

本文永久更新連結地址:https://www.bkjia.com/Linux/2018-03/151358.htm

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.