u1、u2、u4分別代表1字節、2字節、4字節
一. 初識字節碼文件
整體結構
1. Class 字節碼中有兩種數據類型
Class文件是一組以8字節爲基礎單位的二進制流,各個數據項目嚴格按照順序緊湊地排列在Class文件之中,中間沒有添加任何分隔符,這使得整個Class文件中存儲的內容幾乎全部是程序運行的必要數據,沒有空隙存在。當遇到需要8位字節以上空間的數據項時,則會按照高位在前的方式分割成若干個8位字節進行存儲。
根據Java虛擬機規範的規定,Class文件格式採用一種類似C語言結構體的僞結構來存儲數據,這種僞結構只有兩種數據類型:無符號數和表:
- 無符號數屬於基本的數據類型,以u1,u2,u4,u8四種,分別連續代表的1個字節、2個字節、4個字節、8個字節組成的整體數據,無符號數可以用來描述文字、索引引用、數量值或者按照UTF-8編碼構成字符串值。
- 表是由多個無符號數或者其他表作爲數據項構成的複合數據類型,所有表都習慣的以“_info” 結尾。表用於描述有層次關係的複合結構的數據,整個Class文件本質上就是一張表。
表(數組):表是由多個基本數據或其他表,按照既定順序組成的大的數據集合。表是有結構的,它的結構體現在:組成表的成分所在的位置和順序都是已經嚴格定義好的。
2. 編寫Java測試類
package com.java.jvm.bytecode;
/**
* @author xuweizhi
* @date 2019/03/03 12:57
*
*
*/
public class ByteCode1 {
private int a = 1;
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
}
3. 查找編譯過後的class文件
由於 Idea j 反編譯工具的存在,查看的字節碼文件是已經經過反編譯後的格式,因此我們要藉助JDK自帶的JavaP工具查看字節碼文件。
進入編譯後的classes 目錄 D:\root\JavaPlus\jvm\out\production\classes>,輸入javap com.java.jvm.bytecode.ByteCode1命令,打印字節碼數據
D:\root\JavaPlus\jvm\out\production\classes>javap com.java.jvm.bytecode.ByteCode1
Compiled from "ByteCode1.java"
public class com.java.jvm.bytecode.ByteCode1 {
public com.java.jvm.bytecode.ByteCode1();
public int getA();
public void setA(int);
}
4. javap -c com.java.jvm.bytecode.ByteCode1
更詳細的的方式打印字節碼文件信息
D:\root\JavaPlus\jvm\out\production\classes>javap -c com.java.jvm.bytecode.ByteCode1
Compiled from "ByteCode1.java"
public class com.java.jvm.bytecode.ByteCode1 {
public com.java.jvm.bytecode.ByteCode1();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field a:I
9: return
public int getA();
Code:
0: aload_0
1: getfield #2 // Field a:I
4: ireturn
public void setA(int);
Code:
0: aload_0
1: iload_1
2: putfield #2 // Field a:I
5: return
}
5. javap -verbose com.java.jvm.bytecode.ByteCode1
D:\root\JavaPlus\jvm\out\production\classes>javap -verbose com.java.jvm.bytecode.ByteCode1
Classfile /D:/root/JavaPlus/jvm/out/production/classes/com/java/jvm/bytecode/ByteCode1.class
Last modified 2019年3月3日; size 495 bytes
MD5 checksum 681ff27fa74e0f13311cfb52a14b3fb0
Compiled from "ByteCode1.java"
public class com.java.jvm.bytecode.ByteCode1
minor version: 0 #次版本號
major version: 52 #主版本號
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #3 // com/java/jvm/bytecode/ByteCode1
super_class: #4 // java/lang/Object
interfaces: 0, fields: 1, methods: 3, attributes: 1
Constant pool:
#1 = Methodref #4.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#21 // com/java/jvm/bytecode/ByteCode1.a:I
#3 = Class #22 // com/java/jvm/bytecode/ByteCode1
#4 = Class #23 // java/lang/Object
#5 = Utf8 a
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/java/jvm/bytecode/ByteCode1;
#14 = Utf8 getA
#15 = Utf8 ()I
#16 = Utf8 setA
#17 = Utf8 (I)V
#18 = Utf8 SourceFile //編譯的源文件
#19 = Utf8 ByteCode1.java
#20 = NameAndType #7:#8 // "<init>":()V # 指向常量池中索引值
#21 = NameAndType #5:#6 // a:I
#22 = Utf8 com/java/jvm/bytecode/ByteCode1
#23 = Utf8 java/lang/Object
{
public com.java.jvm.bytecode.ByteCode1();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field a:I
9: return
LineNumberTable:
line 9: 0
line 11: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/java/jvm/bytecode/ByteCode1;
public int getA();
descriptor: ()I
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field a:I
4: ireturn
LineNumberTable:
line 14: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/java/jvm/bytecode/ByteCode1;
public void setA(int);
descriptor: (I)V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: iload_1
2: putfield #2 // Field a:I
5: return
LineNumberTable:
line 18: 0
line 19: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lcom/java/jvm/bytecode/ByteCode1;
0 6 1 a I
}
SourceFile: "ByteCode1.java"
subline 或者 Notepad++ 插件 Hex Editor 或者 Winhex 查看16進制文件
二. Javap 命令
- 使用javap -verbose 命令分析一個字節碼文件時,將會分析該字節碼文件的魔數、版本號、常量池、類信息、類的構造方法、類中的方法信息、類變量與成員變量等信息。
三. .class二進制文件
Java .class 文件是以最小單位爲兩個16位字母構成的16進制文件,由魔數來看,前八個字母構成了4個字節。
3.1 魔數
所有的.class字節碼文件的前4個字節都是魔數,魔數值爲固定值:0xCA FE BA BE(咖啡寶貝)
3.2 版本號
魔數之後的四個字節爲版本信息,前兩個字節表示minor version(次版本號),後兩個字節表示major version(主版本號),換算成十進制,表示次版本號爲0,主版本號爲52.所以該版本爲:1.8.0,通過java -version 驗證 1.8.0_181
表示.class是由哪個具體版本的JDK編譯而成的十六進制文件。
次版本號:第 5、6 個字節 00 00
主版本號:第 7、8 個字節 00 34
3.3 常量池(constant pool)
3.3.1 常量池概念
緊接着主版本號之後的就是常量池入口
一個Java類中定義的很多信息都是由常量池來維護和描述的,可以將常量池看作Class文件的資源倉庫,比如Java類中定義的方法與變量信息,都是存儲在常量池中。
常量池主要存儲兩類常量:字面量與符號引用
- 字面量:如文本字符串,Java中聲明爲final的常量值等
字面量是指由字母,數字等構成的字符串或者數值,它只能作爲右值出現,所謂右值是指等號右邊的值,如:int a=123這裏的a爲左值,123爲右值。
常量和變量都屬於變量,只不過常量是賦過值後不能再改變的變量,而普通的變量可以再進行賦值操作。
例:
int a;//a變量
const int b=10;//b爲常量,10爲字面量
string str="hello world";//str爲變量,hello world爲也字面量
- 符號引用:如類和接口的全侷限定名,字段的名稱和描述符,方法的名稱和描述符。
3.3.2 常量池的總體結構
Java類所對應的常量池主要由常量池數量與**常量池數組(常量表)**這兩部分共同構成。
- 常量池數量緊跟在主版本號後面,佔據兩個字節
- 常量池數組(常量表)則緊着在常量池數量之後,常量池數組與一般的數組不同的是,常量池數組中不同的元素的類型、結構都是不同的,長度當然也就不同;但是,每一種元素的第一個數據都是u1類型,該字節是個標誌位,佔據1個字節。JVM在解析常量池時,會根據u1類型來獲取元素的具體類型。值得注意的是,常量池數組中元素的個數 = 常量池數 - 1 (其中0暫時不可用),在Class文件格式規範制定之時,設計者將第0項常量空出來是有特殊考慮的,這樣做的目的是滿足某些常量池索引值的數據在特定情況下需要表達“不引用任何一個常量池”的含義;根本原因在於,索引爲0也是一個常量(保留常量),只不過它不位於常量表中,這個常量就對應null值;所以,常量池的索引從1而非0開始。在整個class文件中,只用常量池的索引從1開始,其它是從0開始。
3.3.4 常量池常量項的結構總表
tag:常量池類型的索引值,用u1(一個字節)表示
3.3.5 常量池信息
cafe babe 0000 0034 0018
0018:表示常量池的數量爲1*16+8=24個,爲什麼java -verbose反編譯的文件只有23個常量呢?
值得注意的是,常量池數組中元素的個數 = 常量池數 - 1 (其中0暫時不可用)。
目的是滿足某些常量池索引值的數據在特定情況下需要表達不引用任何一個常量池的含義;根本原因在於,索引爲0也是一個常量(保留常量),只不過它不位於常量表中,這個常量就對應null值;所以,常量池的索引從1而非0開始
3.3.6 字節碼文件分析
在JVM規範中,每個變量/字段都有描述信息,描述信息主要的作用是描述字段的數據類型、方法的參數列表(包括數量、類型與順序)與返回值。根據描述符規則,基本數據類型和代表無返回值的void類型都用一個大寫字符來表示,對象類型則使用字符L加對象的全限定名稱來表示。爲了壓縮字節碼文件的體積,對於基本數據類型,JVM都只使用一個大寫字母來表示,如下所示:
- B - byte
- C - char
- D - double
- I - int
- F - float
- J - long
- S - short
- Z - boolean
- v - void
- L - 對象類型 如: Ljava/lang/String;
對於數組類型來說,每一個維度使用一個前置的[來表示,如int[]被記錄爲[I,String[][]被記錄爲[[java.lang.String
用描述符描述方法時,按照先參數列表,後返回值的順序來描述。參數列表按照參數的嚴格順序放在一組()之內,如方法:String getRealnamebyIdAndNickname(int id , String name)的描述符爲:(I,Ljava/lang/String;)Ljava/lang/String;
第一個常量分析 0a 00 04 00 14 (5個常量)
CONSTANT_Methodref_info
tag(u1): 0a(10) 對應tag值的表格類型爲CONSTANT_Methodref_info
index: 00 04
idnex: 00 14
cafe babe 0000 0034 0018 0a00 0400 1409
0003 0015 0700 1607 0017 0100 0161
Constant pool:
#1 = Methodref #4.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#21 // com/java/jvm/bytecode/ByteCode1.a:I
#3 = Class #22 // com/java/jvm/bytecode/ByteCode1
#4 = Class #23 // java/lang/Object
#5 = Utf8 a
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/java/jvm/bytecode/ByteCode1;
#14 = Utf8 getA
#15 = Utf8 ()I
#16 = Utf8 setA
#17 = Utf8 (I)V
#18 = Utf8 SourceFile
#19 = Utf8 ByteCode1.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = NameAndType #5:#6 // a:I
#22 = Utf8 com/java/jvm/bytecode/ByteCode1
#23 = Utf8 java/lang/Object
3.4 訪問標誌
訪問標誌信息包括Class文件是類還是接口,是否被定義成public,是否是abstract,如果是類,我們是否被聲明成final.通過上面的源代碼,我們知道該文件是類還是public,佔據兩個字節。
0x 0021:是 0x 0020 和 0x 0001 並集,表示ACC_PUBLIC與ACC_SUPER
3.5 class類名
佔據兩個字節,表示常量池所在數據的索引值,如0x 00 03 表示常量池中所在的索引對應的數據
#3 = Class #22 // com/java/jvm/bytecode/ByteCode1
3.6 父類
佔據兩個字節,一樣表示父接口指向常量池所處的索引值
3.7 接口
佔據兩個字節,表示接口的數量。若接口的數量等於0,則表示沒有接口,其後面的接口字節碼文件沒有數據。
3.8 字段表集合
字段表用於描述類和接口中聲明的變量,這裏的字段包含了類級別變量以及實例變量,但是不包括方法內部聲明的局部變量。
兩個字節表示字段數量,字段表的結構
field_info{
u2 access_flags; 0002 # 表示字段的修飾符
u2 name_index; 0005 # 表示字段的簡單名稱
u2 decriptor_index; 0006 # 表示字段和方法的描述符
u2 attributes_count; 0000
attribute_info attributes[attributes_count];
}
- 全限定名:以"org/fenixsoft/calzz/TestClass"是這個類的全限定名,僅僅是把類全名中的".“替換成了”/"而已,爲了使連續的多個全限定名之間不產生混淆,在使用最後一般會加入一個“;”表示全限定名結束。
- 簡單名稱:指沒有類型和參數修飾的方法或者字段名稱,這個類中inc()方法和m字段的簡單名稱分別爲是"inc"和"m".
- 描述符:描述符的作用是用來描述字段的數據類型、方法的參數列表(包括數量、類型以及順序)和返回值。根據描述規則,基本數據類型爲(byte、char、int、long、long、short、boolean)以及所表示無返回值的void類型都用一個大寫字符來表示,而對象類型則是用符號L加加對象的全限定名來表示,詳情見下圖:
對於數組類型,每一維度將使用一個前置的“[”字符來描述,如定義一個爲“java.lang.String[]”,類型的二維數組,將被記錄爲:"[[Ljava/lang/String",一個整型數組"int[]“將被標記爲”[I".
用描述符來描述方法,按照先參數列表,後返回值的順序描述,參數列表按照參數的嚴格順序放在一組小括號內“()”之內。如方法void inc()的描述符“()V”,方法java.lang.String.toString()?)Ljava/lang/String;
00 01 00 02 00 05 00 06 00 00
- 00 01:表示字段數量
- 00 02:訪問修飾符 private
- 00 05:字段名稱索引 a
- 00 06:描述符索引,對應常量池數據 I => int
- 00 00:
3.9 方法
u2:佔據兩個字節表示方法的數量
方法表結構
3.9.1 參照方法表結構進行分析
method_info {
u2 access_flags; #訪問標識符
u2 name_index; #屬性名索引
u2 description_index; #描述符對應java類型
u2 attributes_count; #屬性數量
attribute_info attributes[attributes_count];
}
00 03 00 01 00 07 00 08 00 01 00 09 00 00 00 38 00 02 00 01 00 00 00 0A 2A B7 00 01 2A 04 B5 00 02 B1 00 00 00 02 00 0A 00 00 00 0A 00 02 00 00 00 09 00 04 00 0B 00 0B 00 00 00 0C 00 01 00 00 00 0A 00 0C
- 00 03:表示三個方法,默認的構造函數
- 00 01:權限修飾符 ACC_PUBLIC
- 00 07: 指向常量池索引
- 00 08:指向常量池索引()v
- 00 01: 表示有一個attribute_info信息,一下是attribute_info數據結構
attribute_info {
u2 attribute_name-index; #屬性名的 索引值
u4 attribute_length; #屬性的長度
u1 info[attribute_length]; #屬性的具體數據
}
- 00 09:表示指向常量池索引值爲 Code 表示方法的代碼
- 00 00 00 38:表示代碼的長度爲56字節
- 00 02:max_stack = 2
- 00 01:max_locals = 1
- 00 00 00 0A:code_length 真正執行的指令碼,也可以稱之爲助記符
- 2A B7 00 01 2A 04 B5 00 02 B1:
- 2A : load_0 Load reference from local variable
- B7 : invokespecial 00 01 常量池索引
- 2A :
- 04 : count iconst_1 = 4 (0x4) 複製
- B5 : putfield 賦值 00 02 對應常量池索引值爲 com/java/jvm/bytecode/ByteCode1.a:I
- B1 :return
- 00 00 : 異常數量爲0
- 00 02 :屬性數量爲2
- 00 0A :LineNumberTable 字節碼與本地Java文件對應的行號關係
- 00 00 00 0A: 屬性字節長度
- 00 02 00 00 00 09 00 04 00 0B :
- 00 02: 表示有兩對映射
- 00 00 00 09:字節碼爲0 Java代碼偏移量爲9
- 00 04 00 08:字節碼爲4 Java代碼偏移量爲8
- 00 0B 00 00 00 0C
- 00 0B: LocalVariableTable 局部變量表
- 00 00 00 0C:12個長度
- 00 01 00 00 00 0A 00 0C 00 0D 00 00
- 00 01 :局部變量個數
- 00 00 00 0A: 開始位置0,結束位置10
- 00:索引0
- 0C: 常量池中對應的索引 this 當前對象
- 00 0D:局部變量的描述 Lcom/java/jvm/bytecode/ByteCode1
- 00 00:jdk1.6加入,動態檢查
00 01 00 0E 00 0F 00 01 00 09 00 00 00 2F 00 01 00 01 00 00 00 05 2A B4 00
對於Java文件來講,默認的無慘方法會隱式傳入this參數,用來調用成員變量,但是對於class文件來說,this這個參數不可被忽略,因此this這個參數代表着那個啥呢?局部變量被方法隱式的加入到了第一個局部變量
對於非靜態方法來講,至少有一個參數傳入
3.9.2 方法的Code表結構
- JVM預定義了部分attribute,但是編譯器自己也可以實現自己的attribute寫入class文件,供運行時使用
- 不同的attribute通過attribute_name_index來區分
3.9.3 Code 結構
Code Attribute 的作用是保存該方法的結構,如所對應的字節碼
Code_attribute {
u2 attribute_name_index; ## 方法一致爲Code
u2 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];
}
- attribute_length:表示attribute所包含的字節數,不包含attribute_name_index和attribute_length字段。
- max_stack:表示這個方法運行的任何時刻所能達到的操作數棧的最大深度
- max_locals:表示方法執行期間創建的局部變量的數目,包含用來表示傳入的參數的局部變量
- code_length:表示該方法所包含的字節碼的字節數以及具體的指令碼,其後爲真正的執行代碼
- 具體字節碼即是該方法被調用時,虛擬機所執行的字節碼
exception_table
- exception_table,這裏存放的是處理異常的信息
- 每個exception_table表項由start_pc,end_pc,handler-pc,catch_pc組成
- start_pc和end_pc表示在code數組中的從start_pc到end_pc(包含start_pc,不包含end_pc)的指令拋出的異常會由這個表項來處理
- handler_pc表示處理異常的代碼的開始處,catch_type表示會被處理的異常類型,它值常量池的一個異常類。當catch_type爲0時,表示處理所有的異常
附加屬性
接下來是該方法的附加屬性
LineNumberTable:這個屬性用來表示code數組中的字節碼和Java代碼行數之間的關係,這個屬性可以用來在調試的時候定位代碼執行的行數
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];
}
ideaj jclasslib插件
https://github.com/ingokegel/jclasslib
jclasslib 插件