每一個 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 等類來實現。html
Class 文件的內容可用一組私有數據類型來表示,它們包括 u1,u2 和 u4,分別代 表了一、2和4個字節的無符號數。在 Java SDK 中這些類型的數據能夠經過實現接口 java.io.DataInput 中的 readUnsignedByte、readUnsignedShort 和 readInt 方法進 行讀取。java
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 文件後,打開:jvm
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 文件來分析每個字節所表明的含義。post
Class 文件的第 1 - 4 個字節表明了該文件的魔數。this
魔數的惟一做用是肯定這個文件是否爲一個能被虛擬機所接受的 Class 文件。魔數值固定爲 0xCAFEBABE,不會改變。url
Class 文件的第 5 - 6 個字節表明了 Class 文件的副版本號。spa
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。
緊跟版本信息以後的是常量池信息,其中前 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 個常量。
緊跟着常量池計數器後面就是 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 出來的就是人家分析總結好的。
在常量池結束以後,緊接着的兩個字節表明類或接口的訪問標記(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 標記會自動忽略它。
在訪問標記後,則是類索引、父類索引、接口索引的數據,這裏數據爲:00 05 、00 06 、00 00。
類索引和父類索引都是一個 u2 類型的數據,而接口索引集合是一組 u2 類型的數據的集合,這個能夠由前面 Class 文件的構成能夠獲得。Class 文件中由這三項數據來肯定這個類的繼承關係。
類索引,this_class 的值必須是對 constant_pool 表中項目的一個有效索引值。 constant_pool 表在這個索引處的項必須爲 CONSTANT_Class_info 類型常量,表示這個 Class 文件所定義的類或接口。這裏的類索引是 00 05 表示其指向了常量池中第 5 個常量,經過咱們以前的分析,咱們知道第 5 個常量其最終的信息是 Log 類。
對於類來講,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 的值表示當前類或接口的直接父接口數量。
interfaces[] 數組中的每一個成員的值必須是一個對 constant_pool 表中項 目的一個有效索引值,它的長度爲 interfaces_count。每一個成員 interfaces[i] 必 須爲CONSTANT_Class_info類型常量,其中0 ≤ i < interfaces_count。在 interfaces[]數組中,成員所表示的接口順序和對應的源 代碼中給定的接口順序(從左至右)同樣,即 interfaces[0]對應的是源代碼中最左 邊的接口。
這裏 Log 類的字節碼文件中,由於並無實現任何接口,因此緊跟着父類索引後的兩個字節是0x0000,這表示該類沒有實現任何接口。所以後面的接口索引表爲空。
字段表集合用於描述接口或者類中聲明的變量,這裏的數據爲:00 00。
fields_count 的值表示當前 Class 文件 fields[]數組的成員個數。 fields[]數組中每一項都是一個 field_info 結構的數據項,它用於表示該類或接口聲明的類字段或者實例字段。
fields[]數組中的每一個成員都必須是一個 fields_info 結構的數 據項,用於表示當前類或接口中某個字段的完整描述。fields[]數組描述當前類或接口 聲明的全部字段,但不包括從父類或父接口繼承的部分。
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
attributes_count 的值表示當前 Class 文件 attributes 表的成員個 數。attributes 表中每一項都是一個 attribute_info 結構的數據項。
屬性表,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 文件的屬性:
在字段表後的 2 個字節是一個方法計數器,表示類中總有有幾個方法,在字段計數器後,纔是具體的方法數據。這裏數據爲:00 02 。
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
Log 類的字節碼文件中,方法計數器的值爲 00 02,表示一共有 2 個方法。
根據前面的結構格式能夠知道,
方法計數器後 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 屬性的格式以下:
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 類文件結構有一個深刻的認識。 最後用一張圖來總結一下: