做爲 一個通用的、機器無關的執行平臺,任何其餘語言的實現者均可以將 Java 虛擬機做爲語言的產品交付媒介。Java 虛擬機不和包括 Java 在內的任何語言綁定,它只與「Class 文件」這種特定的二進制文件格式所關聯,Class 文件中包含了 Java 虛擬機指令集和符號表以及若干其餘輔助信息html
當前使用 JVM 的語言有 Java、JRuby、Groovy 等等java
[TOC]數組
class 文件可能由類加載器生成,因此並非全部類或者接口都定義在已有的 class 文件中markdown
爲了解決大端小端的問題,class 文件規定高位字節放在文件靠前的位置架構
class 文件中主要包含兩種數據:oracle
_info
做爲後綴class 文件的大體結構以下,後面詳細介紹:jvm
0xcafebabe
使用 javac
命令編譯下面的代碼(我所使用的 javac 版本:javac 1.8.0_65
,win7x64)函數
package xyz.yearn; public class TestClass { private int m; public int inc(){ return m+1;} }
class 文件的二進制內容爲(可使用 Free Hex Editor Neo 查看,爲了方便閱讀,講解的部分使用雙下劃線標出)this
class 文件的二進制內容和下面的 表1 是對應的編碼
// 主版本號 0x34;常量池中有 0x13-1=18 個表項;第一個表項爲 0xa,即 `methodref_info` ca fe ba be 00 00 __00 34 00 13 0a__ 00 04 00 0f 09 00 03 00 10 07 00 11 07 00 12 01 00 01 6d 01 00 01 49 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65 01 00 03 69 6e 63 01 00 03 28 29 49 01 00 0a 53 6f 75 72 63 65 46 69 6c 65 01 00 0e 54 65 73 74 43 6c 61 73 73 2e 6a 61 76 61 0c 00 07 00 08 0c 00 05 00 06 01 00 13 78 79 7a 2f 79 65 61 72 6e 2f 54 65 73 74 43 6c 61 73 73 01 00 10 6a 61 76 61 2f 6c 61 6e 67 // 常量池以後爲訪問標誌位,即下面的 0x0021 // 將上面類前的 public 刪除(或者改成 private),0x0021 將變爲 0x0020(private) // 訪問標誌後爲類索引(u2,即 0x0003)、父類索引(u2,即 0x0004)和接口索引 // 接口索引第一個 u2 爲實現接口的個數,0x0000,表示當前類未實現任何接口 2f 4f 62 6a 65 63 74 __00 21 00 03 00 04 00 00__ // 接口索引後爲字段表,第一個 u2 用於計數;餘下的 3 個 u2 表示變量 m 的訪問標誌和兩個索引 // 每個字段均可以有額外的描述信息,m第三個u2後爲這些信息的計數,不過 m 沒有,因此值爲 0x0000 __00 01 00 02 00 05 00 06 00 00__ // 字段表後爲方法表集合,第一個 u2(0x0002) 爲方法計數,一個爲構造函數 init,一個爲 inc // 0x0001 表示 public;0x0007 爲函數名索引;0x0008 爲函數類型描述 __00 02 00 01 00 07 00 08 00 01 00 09__ 00 00 00 1d 00 01 00 01 00 00 00 05 2a b7 00 01 b1 00 00 00 01 00 0a 00 00 00 06 00 01 00 00 00 03 00 01 00 0b 00 0c 00 01 00 09 00 00 00 1f 00 02 00 01 00 00 00 07 2a b4 00 02 04 60 ac 00 00 00 01 00 0a 00 00 00 06 00 01 00 00 00 05 00 01 00 0d 00 00 00 02 00 0e
得到 class 文件後執行命令:javap -verbose TestClass
,能夠獲得下面的輸出(有刪減,全文使用表 1引用下面輸出)
原始的 class 文件是二進制的,使用上面的命令能夠將其解析爲便於閱讀的文本格式(不一樣版本的 Java,解析出來的格式不一樣)
// class 文件前 4 個字節爲魔數 0xcafebabe,用於標識當前文件爲 Java 所用的 class 文件 public class xyz.yearn.TestClass minor version: 0 // 第 5 和第 6 個字節用於標識次版本號,class 使用大端法,故靠左的字節爲高位字節 major version: 52 // 第 7 和第 8 字節用於標識主版本號 flags: ACC_PUBLIC, ACC_SUPER // 這是解析 訪問標誌位 得到的信息 Constant pool: // 主版本號以後即爲常量池入口 #1 = Methodref #4.#15 // java/lang/Object."<init>":()V #2 = Fieldref #3.#16 // xyz/yearn/TestClass.m:I #3 = Class #17 // xyz/yearn/TestClass #4 = Class #18 // java/lang/Object #5 = Utf8 m // 變量 m 的變量名 #6 = Utf8 I // 變量類型,I 表示 int,數組使用[、[[等描述 #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 inc #12 = Utf8 ()I #13 = Utf8 SourceFile #14 = Utf8 TestClass.java // 在二進制文件中,這句後面爲訪問標誌 #15 = NameAndType #7:#8 // "<init>":()V #16 = NameAndType #5:#6 // m:I #17 = Utf8 xyz/yearn/TestClass #18 = Utf8 java/lang/Object { public xyz.yearn.TestClass(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 // init 沒有參數,args_size 和 locals 中的 1 表示 this 指針 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 5: 0 public int inc(); descriptor: ()I flags: ACC_PUBLIC Code: // 棧幀所需的局部變量表、操做數個數在編譯時已經肯定並寫在 code 屬性中 // Java 的指令面向操做數棧而非寄存器,因此須要一塊內存(stack) 保存指令的操做數 // 操做數棧最大深度編譯時已肯定,由於 Java 指令操做數的個數是肯定的 stack=2, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field m:I 4: iconst_1 5: iadd 6: ireturn LineNumberTable: line 7: 0 }
常量池中保存兩大類數據:字面值與表,表的類型個數是有限的,JDK 7 中有 14 種,常量池的索引值從 1 開始
表的第一個字節(u1)標識表的類型,下面簡要介紹幾種表
CONSTANT_Methodref_info
用於保存類中方法的符號引用
類型 | 名稱 | 數量 | 備註 |
---|---|---|---|
u1 | tag | 1 | 標識類型,當前值爲 10 |
u2 | index | 1 | 索引,指向聲明方法的類描述符 Class_info |
u2 | index | 1 | 索引,指向名稱及類型描述符 |
CONSTANT_Class_info
此類型的常量表明一個類或者接口的符號引用,其結構以下
類型 | 名稱 | 數量 | 備註 |
---|---|---|---|
u1 | tag | 1 | 標識類型,當前值爲 7 |
u2 | name_index | 1 | 索引,執行一個 utf8 表,指向的表保存類或接口的全限定名 |
表 1 中的 #3
即爲一個 class_info
表,其中的 name_index
索引指向了 #17
,#17
即爲一個 utf8_info
表
CONSTANT_Utf8_info
此類型用於保存 utf8 編碼的字符串,其結構以下。Java 中變量名由當前表保存,故Java中符號名最長爲 64K
類型 | 名稱 | 數量 | 備註 |
---|---|---|---|
u1 | tag | 1 | 當前值爲 1 |
u2 | length | 1 | 表示後續佔用字節數,length 最大值爲64K |
u1 | bytes | length | 實際的字符串內容 |
Java 使用字符串描述變量與方法的類型
Java 使用指定字符表示變量的基本類型,例如 B 表示 byte、C 表示 char:
B:byte, C:char, D:double, F:float, I:int, J:long, S:short, Z:boolean, V:void, L:object
對於數組,每一維使用一個前置 [
來描述,例如:java.lang.String[][]
,其描述爲:[[Ljava/lang/String;
用描述符來描述方法時,按照先參數列表,後返回值的順序描述,參數列表按照參數的嚴格順序放在一組小括號「()」 以內。如方法 void inc()
的描述符爲()V
,方法 java.lang.String toString()
的描述符爲()Ljava/lang/String;
,方法 int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)
的描述符爲([CII[CIII)I
在常量池結束以後,緊接着的兩個字節表明訪問標誌(access_flags
),這個標誌用於識別一些類或者接口層次的訪問信息,包括:這個 Class 是類仍是接口;是否認義爲 public 類型;是否認義爲 abstract 類型;若是是類的話,是否被聲明爲 final 等
父類索引、類索引與接口索引用於指明當前類或接口的繼承關係
因爲 Java 語言不容許多重繼承,因此父類索引只有一個,除了 java.lang.Object
以外,全部的 Java 類都有父類,所以除了 java.lang.Object
外,全部 Java 類的父類索引都不爲 0
接口索引集合就用來描述這個類實現了哪些接口,這些被實現的接口將按 implements 語句(若是這個類自己是一個接口,則應當是 extends 語句)後的接口順序從左到右排列在接口索引集合中
字段表(field_info
)用於描述接口或者類中聲明的變量。字段(field)包括類級變量以及實例級變量,但不包括在方法內部 聲明的局部變量
字段表集合中不會列出從超類或者父接口中繼承而來的字段,但有可能列出本來 Java 代碼之中不存在的字段,譬如在內部類中爲 了保持對外部類的訪問性,會自動添加指向外部類實例的字段
類型 | 名稱 | 數量 | 備註 |
---|---|---|---|
u2 | access_flags | 1 | 當前字段的訪問標誌 |
u2 | name_index | 1 | 字段名稱 |
u2 | descriptor_index | 1 | 字段和方法的描述 |
class 文件存儲格式中對方法的描述與對字段的描述幾乎採用了徹底一致的方式,方法表的結構如同字段表同樣,依次包括了訪問 標誌(access_flags
)、名稱索引(name_index
)、描述符索引(descriptor_index
)、 屬性表集合(attributes)幾項
方法的具體代碼保存在下面的屬性表集合中
屬性表用於保存其餘字段中的一些屬性,例如 Code 屬性用於保存方法的代碼
Java 虛擬機採用面向操做數棧而不是寄存器的架構,因此大多數的指令都不包含操做數,只有一個操做碼
Java 虛擬機指令操做碼長度只有一個字節,因此指令集的操做碼總數不超過 256 條
由於 Java 操做碼個數有限,故不可能每一種類型的數據都對應獨立的操做碼,因此不少不一樣類型的數據所使用的操做碼是相同的
JVM 中已有指令詳細介紹可參看其餘資料,本文只介紹部分後續會用到的指令
與常見的 CPU 指令相比, JVM 指令有一個異常拋出命令:athrow
雖然類實例和數組都是對象,但 Java 虛擬機對類實例和數組的建立與操做使用了不一樣的字節碼指令
類建立指令:new
數組建立指令:newarray、anewarray、multianewarray
訪問字段:getfield、putfield、getstatic、putstatic
static
field from class ,參考。靜態初始化塊只會在第一次初始化類時執行