?我們知道Java是一門跨平臺的語言,我們編寫的Java代碼會被編譯成中間class文件以讓Java虛擬機解析運行。而Java虛擬機規范僅僅描述了抽象的Java虛擬機,在實現具體的Java虛擬機時,僅指出了設計規范。Java虛擬機的實現必須體現規范中的內容,但僅在確有必要時才應該受制于這些規范。對于完整內容,可以查看原文檔,以JDK7為例,可查看https://docs.oracle.com/javase/specs/jvms/se7/html/,或者《深入理解Java虛擬機 JVM高級特性與最佳實踐》一書。完整的規范主要包含以下內容:
- 第2章:概覽Java虛擬機整體架構
- 第3章:介紹如何將Java語言編寫的程序轉換為虛擬機指令集
- 第4章:定義class文件格式。它是一種與硬件和操作系統無關的二進制格式,用來表示編譯后的類和接口
- 第5章:定義了Java虛擬機啟動以及類和接口的加載、鏈接和初始化的過程
- 第6章:定義了Java虛擬機指令集
- 第7章:提供了一張以操作碼值為索引的Java虛擬機操作碼助記表
?本文只是大概記錄項目需要了解的基礎概念,著重在介紹Class文件格式上,為該系列后續內容做鋪墊。
?Class文件是一組以8字節為基礎單位的二進制流,各個數據項目嚴格按照順序緊湊排列在class文件中,中間沒有任何分割符。每個 Class 文件都是由 8 字節為單位的字節流組成,所有的 16 位、32 位和 64 位長度的數據將被構造成 2 個、4 個和 8 個 8 字節單位來表示。
?每一個Class文件對應于一個如下所示的ClassFile結構體:

涉及到的內容包括:
- magic:魔數,魔數的唯一作用是確定這個文件是否為一個能被虛擬機所接受的Class文件。魔數值固定為0xCAFEBABE,不會改變。
- minor_version、major_version:副版本號和主版本號,minor_version和major_version的值分別表示Class文件的副、主版本。一個Java虛擬機實例只能支持特定范圍內的主版本號(Mi至Mj)和0至特定范圍內(0至m)的副版本號。
- constant_pool_count:常量池計數器,constant_pool_count的值等于constant_pool表中的成員數加1。
- constant_pool[]:常量池,constant_pool是一種表結構,它包含Class文件結構及其子結構中引用的所有字符串常量、類或接口名、字段名和其它常量。
- access_flags:訪問標志,access_flags是一種掩碼標志,用于表示某個類或者接口的訪問權限及基礎屬性。
- this_class:類索引
- super_class:父類索引
- interfaces_count:接口計數器,interfaces_count的值表示當前類或接口的直接父接口數量
- interfaces[]:接口表,在interfaces[]數組中,成員所表示的接口順序和對應的源代碼中給定的接口順序(從左至右)一樣,即interfaces[0]對應的是源代碼中最左邊的接口。
- fields_count:字段計數器,fields_count的值表示當前Class文件fields[]數組的成員個數。
- fields[]:字段表,fields[]數組描述當前類或接口聲明的所有字段,但不包括從父類或父接口繼承的部分。
- methods_count:方法計數器,methods_count的值表示當前Class文件methods[]數組的成員個數。
- methods[]:方法表,methods[]數組只描述當前類或接口中聲明的方法,不包括從父類或父接口繼承的方法。
- attributes_count:屬性計數器,attributes_count的值表示當前Class文件attributes表的成員個數。
- attributes[]:屬性表
?可用jdk自帶的javap命令對class文件進行反編譯,以查看內容,如下代碼:
public class Ex { public void judgeAge(int age) { int step = 0; if (age > 18) { step++; System.out.println("a litter old"); } else { System.out.println("a litter cute"); step++; } System.out.println(step); } public static void main(String[] args) { Ex ex = new Ex(); ex.judgeAge(16); }}
執行 javap -verbose -p Ex.class的結果為
Classfile Ex.class Last modified 2019-11-29; size 788 bytes MD5 checksum 8b5d8ebf38c4441fe7150c10da31ce1b Compiled from "Ex.java"public class Ex minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPERConstant pool: #1 = Methodref #10.#31 // java/lang/Object."":()V #2 = Fieldref #32.#33 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #34 // a litter old #4 = Methodref #35.#36 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = String #37 // a litter cute #6 = Methodref #35.#38 // java/io/PrintStream.println:(I)V #7 = Class #39 // Ex #8 = Methodref #7.#31 // Ex."":()V #9 = Methodref #7.#40 // Ex.judgeAge:(I)V #10 = Class #41 // java/lang/Object #11 = Utf8 #12 = Utf8 ()V #13 = Utf8 Code #14 = Utf8 LineNumberTable #15 = Utf8 LocalVariableTable #16 = Utf8 this #17 = Utf8 LEx; #18 = Utf8 judgeAge #19 = Utf8 (I)V #20 = Utf8 age #21 = Utf8 I #22 = Utf8 step #23 = Utf8 StackMapTable #24 = Utf8 main #25 = Utf8 ([Ljava/lang/String;)V #26 = Utf8 args #27 = Utf8 [Ljava/lang/String; #28 = Utf8 ex #29 = Utf8 SourceFile #30 = Utf8 Ex.java #31 = NameAndType #11:#12 // "":()V #32 = Class #42 // java/lang/System #33 = NameAndType #43:#44 // out:Ljava/io/PrintStream; #34 = Utf8 a litter old #35 = Class #45 // java/io/PrintStream #36 = NameAndType #46:#47 // println:(Ljava/lang/String;)V #37 = Utf8 a litter cute #38 = NameAndType #46:#19 // println:(I)V #39 = Utf8 Ex #40 = NameAndType #18:#19 // judgeAge:(I)V #41 = Utf8 java/lang/Object #42 = Utf8 java/lang/System #43 = Utf8 out #44 = Utf8 Ljava/io/PrintStream; #45 = Utf8 java/io/PrintStream #46 = Utf8 println #47 = Utf8 (Ljava/lang/String;)V{ public Ex(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return LineNumberTable: line 1: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this LEx; public void judgeAge(int); descriptor: (I)V flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=2 0: iconst_0 1: istore_2 2: iload_1 3: bipush 18 5: if_icmple 22 8: iinc 2, 1 11: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 14: ldc #3 // String a litter old 16: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 19: goto 33 22: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 25: ldc #5 // String a litter cute 27: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 30: iinc 2, 1 33: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 36: iload_2 37: invokevirtual #6 // Method java/io/PrintStream.println:(I)V 40: return LineNumberTable: line 4: 0 line 5: 2 line 6: 8 line 7: 11 line 9: 22 line 10: 30 line 12: 33 line 13: 40 LocalVariableTable: Start Length Slot Name Signature 0 41 0 this LEx; 0 41 1 age I 2 39 2 step I StackMapTable: number_of_entries = 2 frame_type = 252 /* append */ offset_delta = 22 locals = [ int ] frame_type = 10 /* same */ public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=1 0: new #7 // class Ex 3: dup 4: invokespecial #8 // Method "":()V 7: astore_1 8: aload_1 9: bipush 16 11: invokevirtual #9 // Method judgeAge:(I)V 14: return LineNumberTable: line 16: 0 line 17: 8 line 18: 14 LocalVariableTable: Start Length Slot Name Signature 0 15 0 args [Ljava/lang/String; 8 7 1 ex LEx;}SourceFile: "Ex.java"
下面對后續需要接觸到的幾項內容做說明。
1. 數據項
?Class文件中有兩種數據類型,分別是無符號數和表:

- 無符號數:屬于基本數據類型,主要可以用來描述數字、索引符號、數量值或者按照UTF-8編碼構成的字符串值,大小使用u1、u2、u4、u8分別表示1字節、2字節、4字節和8字節
- 表:是由多個無符號數或者其他表作為數據項構成的復合數據類型,所有的表都習慣以“_info”結尾
2. 訪問和修飾符標識

?帶有 ACC_SYNTHETIC 標志的部分,意味著它是由編譯器自己產生的而不是由程序員編寫的源代碼生成的。有該標志的類、屬性、方法等不會在源碼中顯示。
3. 類型描述符

?基本類型的描述是單個字符:
- Z表示 boolean
- C表示 char
- B表示 byte
- S表示 short
- I 表示 int
- F 表示 float
- J 表示 long
- D 表示 double
- 一個類類型的描述符是這個類的內部名,前面加上字符 L,后面跟有一個分號。例如,String 的類型描述符為 Ljava/lang/String;。而一個數組類型的 述符是一個方括號后面跟有該數組元素類型的描述符。
4. 方法描述符
?方法描述符是一個類型描述符列表,它用一個字符串描述一個方法的參數類型和返回類型。
?方法描述符以左括號開頭,然后是每個形參的類型描述,然后是一個右括號,接下來是返回類型的類型描述符,如果該方法返回void,則是 V,表示方法描述中不包含方法的名字或參數名,可看如下例子:

5. 指令
?Java虛擬機的指令由一個字節長度的、代表著某種特定操作含義的操作碼(Opcode)以及跟隨其后的零至多個代表此操作所需參數的操作數(Operands)所構成。虛擬機中許多指令并不包含操作數,只有一個操作碼。
?常見的指令如下:
- 字段訪問指令:getfield,putfield,getstatic,pustatic
- 比較指令:dcmpg,dcmpl,fcmpg,fcmpl,lcmp
- 跳轉指令:ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnonnull
- 比較條件跳轉指令:ificmpeq,ificmpne,ificmplt,ificmpgt,ificmple,ificmpge,ifacmpeq,ifacmpne
- 多條件分支跳轉:tableswitch和lookupswitch
- 無條件跳轉:goto
- 函數調用與返回指令
- 函數調用指令:invokevirtual,invokeinterface,invokespecial,invokestatic,invokedynamic;
- 函數返回:需要將返回值壓入調用者操作數棧,需要使用xreturn指令(x可以是i,l,f,d,a或空)
?PS:筆者個人習慣使用Bytecode Outline進行反編譯,這款插件輸出的內容可讀性會高點,上面的內容輸出下:
// class version 52.0 (52)// access flags 0x21public class Ex { // compiled from: Ex.java // access flags 0x1 public ()V L0 LINENUMBER 1 L0 ALOAD 0 INVOKESPECIAL java/lang/Object. ()V RETURN L1 LOCALVARIABLE this LEx; L0 L1 0 MAXSTACK = 1 MAXLOCALS = 1 // access flags 0x1 public judgeAge(I)V L0 LINENUMBER 4 L0 ICONST_0 ISTORE 2 L1 LINENUMBER 5 L1 ILOAD 1 BIPUSH 18 IF_ICMPLE L2 L3 LINENUMBER 6 L3 IINC 2 1 L4 LINENUMBER 7 L4 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; LDC "a litter old" INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V GOTO L5 L2 LINENUMBER 9 L2 FRAME APPEND [I] GETSTATIC java/lang/System.out : Ljava/io/PrintStream; LDC "a litter cute" INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V L6 LINENUMBER 10 L6 IINC 2 1 L5 LINENUMBER 12 L5 FRAME SAME GETSTATIC java/lang/System.out : Ljava/io/PrintStream; ILOAD 2 INVOKEVIRTUAL java/io/PrintStream.println (I)V L7 LINENUMBER 13 L7 RETURN L8 LOCALVARIABLE this LEx; L0 L8 0 LOCALVARIABLE age I L0 L8 1 LOCALVARIABLE step I L1 L8 2 MAXSTACK = 2 MAXLOCALS = 3 // access flags 0x9 public static main([Ljava/lang/String;)V L0 LINENUMBER 16 L0 NEW Ex DUP INVOKESPECIAL Ex. ()V ASTORE 1 L1 LINENUMBER 17 L1 ALOAD 1 BIPUSH 16 INVOKEVIRTUAL Ex.judgeAge (I)V L2 LINENUMBER 18 L2 RETURN L3 LOCALVARIABLE args [Ljava/lang/String; L0 L3 0 LOCALVARIABLE ex LEx; L1 L3 1 MAXSTACK = 2 MAXLOCALS = 2}
更多原創內容請搜索微信公眾號:啊駝(doubaotaizi)