1.流程
-
創建 HelloWorld.java
代碼如下
package jvm;/** * @author lidiqing * @since 2017/3/4. */ public class HelloWorld { private final String text = "Hello World!"; public HelloWorld() { } public String getText() { return text; } }
編譯成 HelloWorld.class
-
使用
javap -verbose HelloWorld.class
輸出字節信息
javap輸出
- 使用 winhex 查看二進制數據
winhex輸出
2.結構詳解
2.1 Class 文件格式
2.1.1 概述
結構:Class 文件 == 8位字節為單位的二進制流 == 無符號數 + 表
- u2, u4 => 無符號數,代表2個字節,u4 代表4個字節
- xxx_info => 表,無符號數或其他表構成,整個 class 就是一張表
- cp_info 常量表
- field_info 字段表
- method_info 方法表
- attribute_info 屬性表
特點:
- 不依賴于任何操作系統
- 數據存儲使用大端字節序
文件格式:
類型 | 名稱 | 數量 |
---|---|---|
u4 | magic | 1 |
u2 | minor_version | 1 |
u2 | major_version | 1 |
u2 | constant_pool_count | 1 |
cp_info | constant_pool | constant_pool-1 |
u2 | access_flags | 1 |
u2 | this_class | 1 |
u2 | super_class | 1 |
u2 | interfaces_count | 1 |
u2 | interfaces | interfaces_count |
u2 | fields_count | 1 |
field_info | fields | fields_count |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
2.2 魔數和版本
2.2.1 概述
魔數
唯一作用:確認該文件能被虛擬機接受
類加載過程中的驗證階段,會判斷該 Magic版本號和編譯器 JDK 有關
2.2.2 數據分析
magic
固定值:0xCAFEBABYminor_version
次版本號:0x0000major_version
主版本號:0x0034
2.2.3 表
Class 文件的版本表
編譯器版本 | -target參數 | 十六進制版本號 | 十進制版本號(主版本號.次版本號) |
---|---|---|---|
JDK 1.7.0 | 不帶(默認 -target 1.7) | 00 00 00 33 | 51.0 |
JDK 1.7.0 | -target 1.6 | 00 00 00 32 | 50.0 |
JDK 1.8.0 | 不帶(默認 -target 1.8) | 00 00 00 34 | 52.0 |
2.3 常量池(常量表集合)
2.3.1 概述
特點:
- Class 文件的資源倉庫
- Class 文件中和其他項目關聯最多
- Class 文件中最大的數據項目之一
主要類型:
- 字面量,語言層面的常量,如文本字符串,final常量(如字符串 'HelloWorld!')
- 符號引用,編譯層面的常量
- 類和接口的全限定名(如
jvm/HelloWorld
) - 字段的名稱和描述符 (如
jvm/Helloworld/text:Ljava/lang/String;
) - 方法的名稱和描述符(如
java/lang/Object."<init>":()V
)
- 類和接口的全限定名(如
作用:
無論是字段表、方法表還是屬性表,只要涉及到一些常量或者描述,都會有個索引指向該表的某個位置
2.3.2 數據分析
在 class 中的二進制數據
constant_pool_count => 0x19
容量計數,常量表從 1 開始計數,所以 0x19 只有 0x18 個常量,十進制 24 個常量
這樣的話,如果因為其他地方用了索引為 0,常量為空,比如 java.lang.Object 對象是所有類的基類,父索引為 0-
constant_pool
對照 2.3.3 的常量池結果總表來進行分析,直接用 javap 輸出 24 個常量如下Constant pool: #1 = Methodref #5.#20 // java/lang/Object."<init>":()V #2 = String #21 // Hello World! #3 = Fieldref #4.#22 // jvm/HelloWorld.text:Ljava/lang/String; #4 = Class #23 // jvm/HelloWorld #5 = Class #24 // java/lang/Object #6 = Utf8 text #7 = Utf8 Ljava/lang/String; #8 = Utf8 ConstantValue #9 = Utf8 <init> #10 = Utf8 ()V #11 = Utf8 Code #12 = Utf8 LineNumberTable #13 = Utf8 LocalVariableTable #14 = Utf8 this #15 = Utf8 Ljvm/HelloWorld; #16 = Utf8 getText #17 = Utf8 ()Ljava/lang/String; #18 = Utf8 SourceFile #19 = Utf8 HelloWorld.java #20 = NameAndType #9:#10 // "<init>":()V #21 = Utf8 Hello World! #22 = NameAndType #6:#7 // text:Ljava/lang/String; #23 = Utf8 jvm/HelloWorld #24 = Utf8 java/lang/Object
Methodref,是方法的符號引用,比如虛擬機調用 invokespecial 指令會用到,比如該類的初始化方法字節碼指令中有這么一條
1: invokespecial #1
這樣會實例化 #1 的方法,而 #1 是 java.lang.Object 的初始化方法,所以該指令執行了 Object 的初始化
Fieldref,是字段的符號引用,比如虛擬機調用 putfield 指令會去解析該符號引用
7: putfield #3
這樣的話會把該符號引用對應的變量,設置為棧幀中的值
NameAndType,字段或者方法的描述,有名稱和描述符組合起來
Class,表示一個類的全限定名
String,表示一個字符串常量
Utf8,使用 utf-8 格式編碼的字符串,最后其他類型的常量都會指向這些值,這是最基本的常量值
Class 文件加載后,常量池的字面量和符號引用會被存入方法區,多個線程共享。方法區又稱為永久帶,GC 基本不在方法區進行垃圾回收。但也會有,比如一些廢棄的常量和無用的類
2.4 訪問標志
2.4.1 概述
識別類或接口層次的訪問信息
u2類型=2個字節=16位可用,目前只定義了8個,可見后面的訪問標志表
2.4.2 數據分析
在 class 中的二進制信息
- access_flags
該值為 0x21 = 0x01| 0x20
0x01 => ACC_PUBLIC
0x20 => ACC_SUPER
對照訪問標志表,可知是一個普通的 public 類
使用 JDK 1.0.2 之后編譯出來的類的 ACC_SUPER 均為真
2.4.3 訪問標志表
標志名稱 | 標志值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否public類型 |
ACC_FINAL | 0x0010 | 是否final類型,只有類可用 |
ACC_SUPER | 0x0020 | JDK1.2 之后必須為真 |
ACC_INTERFACE | 0x0200 | 標記是接口 |
ACC_ABSTRACT | 0x400 | 是否abstract類型,接口或抽象類為真,其他類為假 |
ACC_SYNTHETIC | 0x1000 | 標記類不是用戶代碼產生 |
ACC_ANNOTATION | 0x2000 | 標記是注解 |
ACC_ENUM | 0x4000 | 標記是枚舉 |
2.5 索引集合
2.5.1 概述
分為三種類型用來確定整個類的繼承關系,屬于類的元數據
- 類索引(this_class)
- 父類索引(super_class)
- 接口索引集合(interfaces)
根據規則,每個類只能有一個父類,但可以有多個接口
而更特殊的 java.lang.Object 是沒有父類的,畢竟是所有類的基類,所以有且僅有 Object 類的父類索引為 0
2.5.2 數據分析
在 class 中的二進制信息
-
this_class => 0x0004
這里指向常量池第4個常量,為 Class,全限定名為 jvm/HelloWorld
使用 javap 解析到的數據Constant pool #4 = Class #23 #23 = Utf8 jvm/HelloWorld
這個索引用來表示該類的全限定名
-
super_class => 0x0005
常量池第5個常量,為 Class,全限定名為 java/lang/Object
使用 javap 解析到的數據Constant pool #5 = Class #24 #24 = Utf8 java/lang/Object
這個索引用來表示該類父類的全限定名,所以該類的父類就是 java/lang/Object
interfaces_count => 0x0000
接口數量為 0
因為沒有實現任何接口interfaces
因為接口數量為 0,所以后面沒有接口數據
2.6 字段表集合
2.6.1 概述
范圍:包含類級、實例級變量,不包括方法內部聲明的局部變量
信息:
使用標記表示
- 作用域
- public
- private
- protect
- static 實例變量還是類變量
- final 可變性
- volatile 并發可見性
- transient 可被序列化
用常量表示
- 字段數據類型(基本類型、對象、數組)
- 字段名稱
2.6.2 數據分析
fields_count => 0x0001
有一個字段-
fields
從 fields_count 知道只有一個字段,查表可以得到 field_info 各個部分為:
access_flags => 0x0012=0x0010|0x0002
根據字段訪問標志可知,這是個 private 和 final 型屬性
name_index => 0x0006
索引對應常量池第6個常量,名為 “text”Constant pool #6 = Utf8 text
descriptor_index => 0x0007
索引對應常量池第7個常量,所以描述符為 “Ljava/lang/String;”
Constant pool
#6 = Utf8 Ljava/lang/String;
attribute_count => 0x0001
所以該字段有一個屬性,接下來就是詳細的 attribute_info 數據,表示該字段的屬性
attribute_name_index => 0x0008
索引對應常量池第8個常量,屬性名稱為 “constantvalue”
Constant pool
#8 = Utf8 constantvalue
該屬性用來通知虛擬機自動為靜態變量賦值。這個發生在類的加載過程中的初始化階段。關于類變量,初始化的時候會給默認值,但如果有 Constantvalue 屬性,就會用這個屬性的值來對類變量進行初始化
attribute_length => 0x00000002
constantvalue 的該值固定為 2,因為后面需要 u2 類型的數據來指向常量池,表示該 constantvalue 的值
constantvalue_index => 0x0002,索引對應常量池第2個常量,值為 “Hello World!”
Constant pool
#2 = String #21
#21 = Utf8 Hello World!
實際上,用代碼進行比較的話,上面字段對應 Java 源碼為
private final String text = "Hello World!";
在編譯成 class 文件后,我們可以解析到這樣的結果
flags: ACC_PRIVATE ACC_FINAL
name: text
descriptor: Ljava/lang/String;
attributes:
constantvalue:HelloWorld!
在 Java 中用 static 和 final 修飾的字段,即該類的常量字段。在 Java 編譯階段的常量傳播優化中,如果 B 只引用了 A 的常量,編譯后 A 的常量會被轉化為 B 的常量,存入 B 類的常量池里。所以,常量傳播優化后,使用者 B 就會擁有 A 的常量,對 A 常量的引用在 Class 文件中成為對自己的常量的引用
2.6.3 表
字段表(field_info):
類型 | 名稱 | 數量 |
---|---|---|
u2 | access_flag | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
字段訪問標志(access_flag):
標志名稱 | 標志值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否 public |
ACC_PRIVATE | 0x0002 | 是否 private |
ACC_PROTECTED | 0x0004 | 是否 protected |
ACC_STATIC | 0x0008 | 是否 static |
ACC_FINAL | 0x0010 | 是否 final |
ACC_VOLATILE | 0x0040 | 是否 volatile |
ACC_TRANSIENT | 0x0080 | 是否 transient |
ACC_SYNTHETIC | 0x1000 | 是否 synthetic |
ACC_ENUM | 0x4000 | 是否 enum |
描述符(descriptor):
標識字符 | 含義 |
---|---|
B | 基本類型 byte |
C | 基本類型 char |
D | 基本類型 double |
F | 基本類型 float |
I | 基本類型 int |
J | 基本類型 long |
S | 基本類型 short |
Z | 基本類型 boolean |
V | 特殊類型 void |
L | 對象類型,如 Ljava/lang/Object |
數組每一維度用 “[” 來描述
比如
-
int[]
=>[I
-
string[][]
=>[[java/lang/String;
-
void inc
=>()V
-
String toString
=>()Ljava/lang/String;
-
int indexof(char[] a, int b, char[] c, int d)
=>([CI[CI)I
類的字段和方法都由兩部分來表示,即名稱和描述符(NameAndType)
2.7 方法表集合
2.7.1 概述
特點:
- 方法中的代碼,被編譯成字節碼后,放在了 “Code” 屬性中
- 如果父類的方法沒有被子類重寫,不會出現父類方法的信息
- 會出現編譯器添加的方法,比如類構造器“
<clinit>
”和實例構造器“<init>
” - Java 文件中同一個類中的方法的特征簽名不能一致;Class 文件特征簽名可以一致,但返回值要不同
特征簽名 => 一個方法中各個參數在常量池中的字段符號的引用集合,不包含返回值
2.7.2 數據分析
methods_count => 0x0002
表示該類有兩個方法-
methods
分別有兩個方法,都是 method_info 的結構。methods_count 緊接著就是第一個方法表
現在只分析第一個方法表
access_flag => 0x0001
方法為 Public 方法
name_index => 0x0009
方法名稱為常量表第9個常量,是 Utf8 類型,為 “<init>”,所以這個方法是類的初始化方法Constant pool #9 = Utf8 <init>
descriptor_index => 0x000A
方法描述符為常量表第10個常量,也是 Utf8 類型,為 “()V”
Constant pool
#10 = Utf8 ()V
attribute_count => 0x0001
屬性數量為 1。從這個可知,該方法有一個屬性。
基本上所有方法都至少有一個屬性 “Code” 用來記錄方法中編譯后的字節碼指令。也可以說,方法中的代碼編譯成字節碼指令后都被存入了 “Code” 屬性中了
attribute_name_index => 0x000B
常量表第11個常量,也是 Utf8 類型,就是 “Code” 屬性
Constant pool
#11 = Utf8 Code
attribute_length => 0x0000003D
表示接下來的 61 個字節就是 “Code” 屬性表的內容了,關于這個屬性表的解析在后面
使用 javap 可以得出詳細的解析結果如下:
2.7.3 表
方法表(method_info):
類型 | 名稱 | 數量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
方法訪問標志(access_flag):
標志名稱 | 標志值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否為 public |
ACC_PRIVATE | 0x0002 | 是否為 private |
ACC_PROTECTED | 0x0004 | 是否為 protect |
ACC_STATIC | 0x0008 | 是否為 static |
ACC_FINAL | 0x0010 | 是否為 final |
ACC_SYNCHRONIZED | 0x0020 | 是否為 synchronized |
ACC_BRIDGE | 0x0040 | 是否為編譯器產生的橋接方法 |
ACC_VARARGS | 0x0080 | 是否接受不定參數 |
ACC_NATIVE | 0x0100 | 是否為 native |
ACC_ABSTRACT | 0x0400 | 是否為 abstract |
ACC_STRICTFP | 0x0800 | 是否為 strictfp |
ACC_SYNTHETIC | 0x1000 | 是否為編譯器自動產生的 |
2.8 屬性表集合
2.8.1 概述
上面已經有用到兩種屬性,字段用到 constantvalue 用來表示是一個常量,方法用到了 code 來記錄方法的字節碼指令集合
特點:
- 屬性不要求嚴格按順序排列,只要不重名
- 屬性值的結構可完全自定義,但虛擬機只取自己認識的
2.8.2 基本結構
類型 | 名稱 | 數量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | info | attribute_length |
每個屬性的名稱都要從常量池中拿一個 Utf8 類型的常量。屬性的內容為 attribute_length 個 info 組成,每個 info 由一個字節組成。所以屬性的組成是很靈活的,如何解釋這些 info,有虛擬機的標準,也可以完全自定義。但虛擬機在解析的時候,只取自己標準的那部分
2.8.3 幾個主要屬性
2.8.3.1 Code 屬性
使用在方法表中,用來表示代碼編譯成的字節碼指令
具體的結構為
類型 | 名稱 | 數量 |
---|---|---|
u2 | attibute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_locals | 1 |
u4 | code_length | 1 |
u1 | code | code_length |
u2 | exception_table_length | 1 |
exception_info | exception_table | exception_length |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
現在拿類第一個方法表的 Code 屬性來進行分析
attribute_name_index 和 attribute_length 上面已經解析過了,為 “Code”,長度為 29。即后面的 29 個字節均為該屬性的內容
max_stack => 0x0002
操作數棧的最大深度,方法執行的任意時刻,操作數棧不會超過這個深度,虛擬機用這個值來分配棧幀(Stack Frame)中的操作數棧深度。這里表示該方法的操作數棧最深為 2。如果虛擬機執行的時候,有出現超出了這個深度,會拋出 Stack Overflow 的異常max_locals => 0x0001
局部變量表所需的存儲空間。該值的單位為 Slot,長度不超過 32 位的數據類型,比如 byte,int 等用一個 Slot,而 double 和 long 用兩個 Slot。這里表示方法局部變量的存儲空間為 1 個Slotcode_length => 0x0000000B
方法體內的字節碼長度,這里的長度為 11,所以接下來 11 位就是字節碼指令了-
code
方法體的字節碼指令,每個指令用一個 u1 表示,即 8 位,一共可以表達 256 條指令,目前虛擬機已經定義 200 多條了。code 指令可以分成以下幾種類型。其中異常指令只記錄顯示拋出(throw)的異常,而像 try...catch 的異常則有屬性表之異常表來處理- 加載和存儲
- 運算
- 類型轉換
- 對象的創建和訪問
- 操作數棧管理
- 控制轉移
- 方法調用和返回
- 異常
- 同步指令
attributes_count => 0x00000002
表示后面還有兩個屬性,對照表可知為 LineNumberTable 和 LocalVariableTable。這兩個屬性都不是必須的,主要用來描述源碼和字節碼之間的一些關系
因為該方法比較簡單,沒有異常等。如果有異常,Code 屬性中還會有異常表 Exception table
2.8.3.2 LineNumberTable 屬性
用處
- 描述 Java 源碼和字節碼行號的對應關系
- 可以使用 javac -g:none 或 -g:lines 選項來取消或顯示。默認顯示
- 用來實現斷點調試(如果沒有的話無法斷點)
LineNumberTable 具體的結構為
類型 | 名稱 | 數量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | line_number_table_length | 1 |
line_number_info | line_number_table | line_number_table_length |
line_number_info 由 start_pc 和 line_number 組成,分別表示字節碼行號和源碼行號
現在拿上面的方法的 LineNumberTable 屬性數據來進行分析
-
attribute_name_index => 0x000C
屬性名稱,指向常量池第 12 個常量,可以得知就是 “LineNumberTable”Constant pool #12 = Utf8 LineNumberTable
attribute_length => 0x0000000E
屬性的長度為 15,所以接下來 15 個字節都是 LineNumberTable 屬性的值line_number_table_length => 0x0003
表示接下來有 3 個的 line_number_info 類型,用來表示字節碼和源碼的對應關系。有可能多個字節指令才會對應一條源碼line_number_table
現在就分析第一個 line_number_info 的信息
start_pc => 0x0000,字節碼第0行
PC 的全稱為 Program Counter,即程序計數,對應程序計數器的值,表示在字節碼指令中的偏移量。因為要保證線程切換后能回到正確的位置,所以每個線程都會有一個程序計數器
line_number => 0x000B,源碼第11行
結合 start_pc 可知,所以0:aload_0
指令對應的源碼為public HelloWorld() {
使用 javap 解析到方法 “<init>” 的完整 LineNumberTable 表如下
有了這些class文件中有了這些信息,我們就可以進行斷點調試了。所以斷點調試是依賴于方法表中是否有設置 LineNumberTable 屬性
2.8.3.3 LocalVariableTable 屬性
用處
- 描述棧幀局部變量表中的變量和 Java 源碼中對應的變量之間的關系
- 可使用 -g:none 或 -g:vars 來取消或者生產這項信息
- 沒有該屬性,當其他人引用這個方法,所有的參數名稱丟失,IDE 會使用 arg0、arg1 等占位符代替
LocalVariableTable 具體的結構為
類型 | 名稱 | 數量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | local_variable_table_length | 1 |
local_variable_info | local_variable_table | local_variable_table_length |
local_variable_info 的結構為
類型 | 名稱 | 數量 |
---|---|---|
u2 | start_pc | 1 |
u2 | length | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | index | 1 |
用第一個方法表的 code 屬性的 LocalVariableTable 屬性數據來分析
attribute_name_index => 0x000D
指向常量池第 13 個常量,可以得知就是 “LocalVariableTable”attribute_length => 0x0000000C
屬性的長度為 12,所以接下來 12 個字節都是 LocalVariableTable 屬性的值local_variable_table_length => 0x0001
表示接下來有一個 local_variable_info 用來表示棧幀內局部變量表和源碼的關系local_varibale_info
start_pc => 0x0000
該局部變量的生命周期開始的字節碼偏移量
length => 0x000B
該局部變量的生命周期作用范圍的長度
所以該局部變量作用在 0 -> 11 條字節碼之間,可知為整個方法的字節碼指令集范圍內
name_index => 0x000E
指向常量池第 14 個常量,為 “this”,所以這個局部變量是 this 指針,指向使用該方法的當前實例
descriptor_index => 0x000F
指向常量池第 15 個常量,為 “Ljvm/HelloWorld
”,所以這個 this 指針代表的就是類 HelloWorld 的實例
index => 0x0000
表示該局部變量在棧幀局部變量表中 Slot 的位置,可知為第一個 Slot
2.8.3.4 ConstantValue 屬性
該屬性在 2.6.2 對字段 “text” 的解析中已經提到,具體的作用是通知虛擬機初始化靜態變量
目前類變量的初始化有兩種方式
- 使用 ConstantValue 屬性
基本類型或者 java.lang.String,同時被 static 和 final 修飾的變量(又稱為常量) - 在類構造函數
<clinit>
中
不是常量,或者不是基本類型或 java.lang.String