一、概念
1.1 無符號數:
以 u1、u2、u3、u4、u8 代表 1 個字節,2 個字節、4 個字節、8 個字節的無符號數。無符號數可以描述數字,索引引用、數量值和按照 UTF-8 編碼構成的字符串值。
1.2 表
- 表是由多個無符號數或其他表作為數據項構成的復合的數據結構,所有表都習慣性的以“_info”結尾。表用于表示有層次關系的復合結構的數據,整個 Class 文件本質上是一張表
1.3 class 文件組成
ClassFile {
u4 magic; //魔數, 用于識別class文件格式
u2 minor_version;//次版本號
u2 major_version;//主版本號
u2 constant_pool_count; //常量池計數器
cp_info constant_pool[constant_pool_count-1]; //常量池
u2 access_flags;//訪問標志
u2 this_class;//類索引
u2 super_class;//父類索引
u2 interfaces_count;//接口計數器
u2 interfaces[interfaces_count];//接口索引集合
u2 fields_count;//字段計數器
field_info fields[fields_count];//字段表集合
u2 methods_count;//方法計數器
method_info methods[methods_count];//方法表
u2 attributes_count; //屬性計數器
attribute_info attributes[attributes_count];附加屬性表
}
1.4 魔數
每個 Class 文件的頭 4 個字節被稱為魔數(Magic Number),它的唯一作用是確定這個文件是否能被虛擬機接受的 Class 文件。它的值是 0xCAFEBABE (咖啡寶貝),非常容易記憶。
1.5 版本號
緊接著的字節是次版本號(minor_version)和主版本號(major_version),Java 的版本號從 45 開始,Java1.1 之后的 JDK 大版本發布主版本號向上加一(Java1.0~Java1.1 使用了 45.0~45.3 的版本號)。注意高版本的 JDK 能向下兼容 以前的 Class 文件,但不能運行以后版本的 Class 文件。
1.6 常量池
常量池可以理解為 Class 文件的資源倉庫,
主要存放:
字面量(Literal)
-
符號引用(Symbolic References)
- 類和接口的全限定名(Full Qualified Name)
- 字段的名稱描述符(Descriptor)
- 方法的名稱和描述符
類型 標識 描述 CONSTANT_Class
7 類或接口的符號引用 CONSTANT_Fieldref
9 字段的符號引用 CONSTANT_Methodref
10 方法的符號引用 CONSTANT_InterfaceMethodref
11 接口中方法的符號引用 CONSTANT_String
8 字符串類型字面量 CONSTANT_Integer
3 整型字面量 CONSTANT_Float
4 浮點型字面量 CONSTANT_Long
5 長整型字面量 CONSTANT_Double
6 雙精度浮點型字面量 CONSTANT_NameAndType
12 字段或方法的部分符號引用 CONSTANT_Utf8
1 UTF-8 編碼字符串 CONSTANT_MethodHandle
15 標識方法句柄 CONSTANT_MethodType
16 標識方法類型 CONSTANT_InvokeDynamic
18 動態方法調用點
1.7 訪問標識(access_flags)
用于識別類和接口層次的訪問信息
Flag Name | Value | Interpretation |
---|---|---|
ACC_PUBLIC |
0x0001 | 是否為被聲明為 public ,可以被其他外部包中訪問 |
ACC_FINAL |
0x0010 | 是否被聲明 final,不能派生子類 |
ACC_SUPER |
0x0020 | Treat superclass methods specially when invoked by the invokespecial instruction. |
ACC_INTERFACE |
0x0200 | 標識一個接口 |
ACC_ABSTRACT |
0x0400 | 聲明 abstract,抽象類,不能實例化 |
ACC_SYNTHETIC |
0x1000 | 聲明 synthetic; 標識這個類并非有用戶代碼產生 |
ACC_ANNOTATION |
0x2000 | 標識這個一個注解 |
ACC_ENUM |
0x4000 | 標識這是一個枚舉 |
1.8 類索引、父類索引和接口索引
Class 文件就是由這三項數據來確定這個類的繼承關系。類索引用于確定類的全限定類名,父索引用于確定父類的全限定類名,接口索引集合用于描述類實現了那些接口。
1.9 字段表集合
字段表集合[field_info] 用于描述接口或者類中聲明的變量。字段(field) 包括類變量和實例變量,但不包括方法內部聲明的局部變量。
-
字段表結構
field_info { u2 access_flags; //訪問標識 u2 name_index; //名稱索引 u2 descriptor_index; //描述符索引 u2 attributes_count; //屬性計數器 attribute_info attributes[attributes_count]; //屬性表 }
-
字段包含的信息:
- 作用域(public 、private、protected 修飾符)
- static 修飾符
- 可變性 final
- 并發可見性 volatile
- 可否序列化 transient
- 字段類型 【基本數據類型(byte、char、short、int、long 、float、double、boolean)、對象、數組】
-
字段訪問標志
ACC_PUBLIC
0x0001 Declared public
; may be accessed from outside its package.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 聲明字段是否是枚舉
簡單名稱:沒有類型和參數修飾的方法或者字段名稱,如 inc()和 m 字段的簡稱為 inc 和 m
全限定名:com/demo/TestClass; “;”標識類的全限定名結束
-
描述符:用于描述字段的數據類型,方法的參數列表(數量、類型、順序)和返回值
標識字符 代表類型 描述 B
byte
基本類型 byte C
char
基本類型 char D
double
基本類型 double F
float
基本類型 float I
int
基本類型 int J
long
基本類型 long L
ClassName;
reference
對象類型,如 : Ljava/lang/Object S
short
基本類型 short Z
boolean
j 基本類型 boolean [
reference
數組類型 ,如數組 int[]
被記錄為 [I,數組String[][]
被記錄為 [[java/lang/StringV void 特殊類型 Void
描述符來描述方法時,按照先參數列表,后返回值的順序描述;如:java.lang.String.toString() 描述為 () Ljava/lang/String,java.lang.String#valueOf(char[], int, int) 描述為 ([CII)Ljava/lang/String
1.10 方法表集合
方法描述采取與字段描述完全一致的方式。
-
方法表結構
method_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
-
相關訪問標識
Flag Name Value Interpretation ACC_PUBLIC
0x0001 方法是否 public ACC_PRIVATE
0x0002 方法是否 private ACC_PROTECTED
0x0004 方法是否 protected
;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_STRICT
0x0800 方法是否 strictfp
;ACC_SYNTHETIC
0x1000 方法是否為 synthetic; -
方法里定義的代碼
方法里面的代碼,經過編譯器編譯成字節指令后,存放在方法屬性表集合,名為 Code 屬性里。
1.11 屬性表集合在
屬性表(attribute_info)在 Class 文件、字段表、方法表中都可以攜帶自己的屬性表集合,用于描述某些場景專有的信息。
-
格式結構
attribute_info { u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length]; }
-
虛擬機預定義屬性
屬性 位置 含義 class 版本 SourceFile
ClassFile
記錄源文件名稱 45.3 InnerClasses
ClassFile
內部類列表 45.3 EnclosingMethod
ClassFile
僅當一個類為局部類或匿名類時才能擁有這個屬性,這個屬性用于標識這個類所在的外圍方法 49.0 SourceDebugExtension
ClassFile
JDK 1.6 中新增的屬性,SourccDcbugExtcnsion 屬性用于在儲額外的調試信息。譬如在進行 JSP 文件調試時,無法通過 Java 堆棧來定位到 JSP 文件的行號, JSR-45 規范為這些非 Java 語言編寫,卻需要編譯成字節碼并運行在 Java 虛擬機中的程序提供了一個進行調試的標準機制,使用 SourccDcbugExtcnsion 屬性就可以用于存儲這個標準所新加入的調試信息 49.0 BootstrapMethods
ClassFile
JDK1.7 新增的屬性,用于保存 invokedynamic 指令引用的引導方法限定符 51.0 ConstantValue
field_info
final 關鍵字定義的常量值 45.3 Code
method_info
Java 代碼編譯成的字節碼指令 45.3 Exceptions
method_info
方法拋出的異常 45.3 RuntimeVisibleParameterAnnotations
,RuntimeInvisibleParameterAnnotations
method_info
JDK5 中新增的屬性,作用于方法參數 RuntimeVisibleParameterAnnotations 屬性指明哪些注解是運行時可見;
RuntimeInvisibleAnnotations`屬性指明哪里注解是運行時不可見的49.0 AnnotationDefault
method_info
JDK1.5 中新增的屬性,用于記錄注解類元素的默認值 49.0 MethodParameters
method_info
MethodParameters 屬性記錄方法的形式參數的信息,比如方法名稱。 52.0 Synthetic
ClassFile
,field_info
,method_info
標識方法或字段為編譯器自動生成的 45.3 Deprecated
ClassFile
,field_info
,method_info
被聲明為 Deprecated 的方法和字段 45.3 Signature
ClassFile
,field_info
,method_info
記錄類,接口,構造函數,方法或字段的簽名 49.0 RuntimeVisibleAnnotations
,RuntimeInvisibleAnnotations
ClassFile
,field_info
,method_info
JDK5 中新增的屬性,為動態注解提供支持。 RuntimeVisibleAnnotations
屬性指明哪些注解是運行時可見;RuntimeInvisibleAnnotations
屬性指明哪里注解是運行時不可見的49.0 LineNumberTable
Code
LineNumberTable 屬性表存放方法的行號信息 45.3 LocalVariableTable
Code
LocalVariableTable 屬性表中存放方法的局部變量信息 45.3 LocalVariableTypeTable
Code
JDK 1.5 中新增的屈件,它使用特征簽名代替描述符,是為了引入泛型語法之后能描述泛型參數化類型而添加 49.0 StackMapTable
Code
JDKL6 中新增的屬性.供新的類型檢查驗證器 (Type Checker)檢查和處理目標方法的后部變量和操作數棧所需要的類型是否匹配 50.0 RuntimeVisibleTypeAnnotations
,RuntimeInvisibleTypeAnnotations
ClassFile
,field_info
,method_info
,Code
jdk8 新增屬性<br /> RuntimeVisibleTypeAnnotations
:運行時可見類型注解<br />RuntimeInvisibleTypeAnnotations
:運行時不可見類型注解52.0
-
Code 屬性
Java 程序方法體中的代碼經過 Javac 編譯器處理后,最終成為字節碼指令存儲在 Code 屬性內。注意并不是所有方法表都存在 Code 屬性,例如,接口和抽象類中的方法就不存在 Code 屬性。
-
Code 屬性格式定義
Code_attribute { u2 attribute_name_index; //指向常量CONSTANT_UTF8_info的索引,常量固定值為Code u4 attribute_length; u2 max_stack; //操作數棧 u2 max_locals; //局部變量表所需的存儲空間 //字節碼長度,最大值可達2^32-1, 但是虛擬機限制了一個方法不允許超過65535條字節碼指令 //即使用了u2 的長度,超出這個限制會導致編譯失敗 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]; }
二、字節碼指令
2.1 加載和存儲指令
加載和存儲指令用于將數據在棧幀中的局部變量表與操作數棧之間傳輸。
-
將局部變量加載到操作數棧
// i 代表對int 操作 // l 代表對long 操作 // f 代表對float 操作 // d 代表對double 操作 // a 代表對引用reference 操作 // iload_<n> 代表一組指令,iload_0、iload_1、iload_2、iload_3等指令 iload iload_<n> lload lload_<n> fload fload_<n> dload dload_<n> aload aload_<n>
-
將數值從操作數棧存儲到局部變量表
istore istore_<n> lstore lstore_<n> fstore fstore_<n> dstore dstore_<n> astore astore_<n>
-
將常量加載到操作數棧
bipush sipush ldc ldc_w ldc2_w aconst_null iconst_ml iconst_<i> lconst_<l> fconst_<f> dconst_<d>
擴充局部變量表的訪問索引的指令:wide
2.2 運算指令
相關指令
-
加法指令
iadd、ladd、fadd、dadd
-
減法指令
isub、 lsub、 fsub、 dsub
-
乘法指令
imul、 lmul、 fmul、 dmul
-
除法指令
idiv、 ldiv、 fdiv、 ddiv
-
求余指令
irem、 lrem、 frem、 drem
-
取反指令
ineg、 lneg、 fneg、 dneg
-
位移指令
ishl、 isbr、 iusbr、 lsbl、 lshr、 lushr
-
按位或指令
ior、 lor
-
按位與指令
iand、 land
-
按位異或指令
ixor、 lxor
-
局部變量自增指令
iinc
-
比較指令
dcmpg、 dcmpl、 fcmpg、 fcmpl、 lcmp
注意
- 只有當除法指令和求余指令遇到除數為零時,虛擬機會拋出 ArithmeticException 異常
- Java 在處理浮點數運算時,不會拋出任何運行異常(Java 語言的異常)
- 當一個操作產生溢出時,將使用有符號的無窮大表示,如果某個操作結果沒有明確的數學定義的話,將會使用 NaN 表示
- 所有使用 NaN 值作為操作數的算術操作,結果都返回 NaN
double a = 1;
double b = a / 0; //不會報錯,結果Infinity
double a = 0.0;
double b = a / 0.0; //不會報錯,結果NaN
2.3 類型轉換指令
類型轉換指令可以將兩種不同的數值類型進行互相轉換,一般用于用戶代碼中的顯示類型轉換操作,隱式類型轉換不同轉換指令,虛擬機直接支持。
-
顯示類型轉換指令
i2b int 轉換byte i2c int 轉換char i2s int 轉換short l2i long 轉換 int f2i float 轉換 int f2l float 轉換 long d2i double 轉換 int d2l double 轉換 long d2f double 轉換 float
-
轉換規則
- 如果浮點值是 NaN, 那轉換結果就是 int 或者 long 類型的 0
- 如果浮點值不是無窮大的話,浮點值使用 IEEE 754 的向零舍入模式去整,獲取整數值 v,如果 v 在目標類型 T(int 或 long) 的標識表示范圍之內,那轉換結果就是 v。
- 否則,將根據 v 的符號,轉換為 T 所能表示的最大或最小正數。
double nan = 0.0 / 0.0; int a = (int) nan; System.out.println(a); //0 float b = (float) nan; System.out.println(b); //NaN
2.4 對象創建與訪問指令
-
創建類實例指令
new
-
創建數組指令
newarray anewarray multianewarray
-
訪問類字段 和 實例字段
getfield putfield getstatic putstatic
-
加載數組元素到操作數棧
baload //byte數組 caload //char數組 saload //short數組 iaload //int數組 laload //long 數組 faload //float 數組 daload //double 數組 aaload //對象數組
-
將操作數棧存儲到數組元素中
bastore castore sastore iastore lastore fastore dastore aastore
-
獲取數組長度
arraylength
-
檢查類實例類型的指令
instanceof checkcast
2.5 操作數棧的管理指令
-
出棧指令
pop pop2 //出棧2個元素
-
復制棧頂一個或者兩個數值并復制或雙份的復制值重新壓入棧頂
dup dup2 dup_x1 dup2_x1 dup_x2 dup2_x2
-
將棧最頂端的兩個數值互換
swap
2.6 控制轉移指令
-
條件分支
ifeq iflt ifle ifne ifge ifnull ifnonull if_icmpeq 比較棧頂兩個int類型數值的大小 ,當前者 等于 后者時,跳轉 if_icmpne if_icmplt if_icmpgt if_icmple if_icmpge if_acmpeq if_acmpne
-
復合條件分支
tableswitch switch 條件跳轉 case值連續 lookupswitch witch 條件跳轉 case值不連續
-
無條件分支
goto 無條件跳轉 goto_w 無條件跳轉 寬索引 jsr SE6之前 finally字句使用 跳轉到指定16位的offset,并將jsr下一條指令地址壓入棧頂 jsr_w SE6之前 同上 寬索引 ret SE6之前返回由指定的局部變量所給出的指令地址(一般配合jsr jsr_w使用) w同局部變量的寬索引含義
2.7 方法調用和返回指令
-
方法調用指令
invokevirtual: 調用對象實例方法 invokeinterface 調用接口方法 invokespecial 調用一些需要特需處理的實例方法,包括實例初始化方法、私有方法、父類方法 invokestatic 調用類方法 invokedynamic 在運行時動態解析出調用點限定符所引用的方法,并執行
-
返回指令
ireturn lreturn freturn dreturn areturn return 聲明為void 的方法
2.8 異常處理指令
athrow 顯示拋出異常
2.9 同步指令
Java 虛擬機可以支持方法級別的同步和方法內部一段指令序列的同步,這兩種同步結構都使用管理(Monitor)來支持。
方法級別的同步是由方法表結構中 ACC_SYNCHRONIZED 訪問標識來處理
-
方法內部一段指令序列的同步
monitorenter 獲取鎖,進入代碼塊 monitorexit 釋放鎖,必須與monitorenter成對出現
-
源碼
public class SynchronizedInstruction { private Object lock=new Object(); void onlyMe(Object lock){ synchronized (lock){ //doSomething } } }
-
反匯編
Compiled from "SynchronizedInstruction.java" public class cn.hdj.jvm.bytecode.SynchronizedInstruction { private java.lang.Object lock; public cn.hdj.jvm.bytecode.SynchronizedInstruction(); void onlyMe(java.lang.Object); } Classfile /home/hdj/IDEA/Java-Learning/src/main/java/cn/hdj/jvm/bytecode/SynchronizedInstruction.class Last modified 2021-3-20; size 488 bytes MD5 checksum 1f6db0fa955b6d719018d2ea50e1e910 Compiled from "SynchronizedInstruction.java" public class cn.hdj.jvm.bytecode.SynchronizedInstruction SourceFile: "SynchronizedInstruction.java" minor version: 0 major version: 51 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #2.#19 // java/lang/Object."<init>":()V #2 = Class #20 // java/lang/Object #3 = Fieldref #4.#21 // cn/hdj/jvm/bytecode/SynchronizedInstruction.lock:Ljava/lang/Object; #4 = Class #22 // cn/hdj/jvm/bytecode/SynchronizedInstruction #5 = Utf8 lock #6 = Utf8 Ljava/lang/Object; #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 onlyMe #12 = Utf8 (Ljava/lang/Object;)V #13 = Utf8 StackMapTable #14 = Class #22 // cn/hdj/jvm/bytecode/SynchronizedInstruction #15 = Class #20 // java/lang/Object #16 = Class #23 // java/lang/Throwable #17 = Utf8 SourceFile #18 = Utf8 SynchronizedInstruction.java #19 = NameAndType #7:#8 // "<init>":()V #20 = Utf8 java/lang/Object #21 = NameAndType #5:#6 // lock:Ljava/lang/Object; #22 = Utf8 cn/hdj/jvm/bytecode/SynchronizedInstruction #23 = Utf8 java/lang/Throwable { private java.lang.Object lock; flags: ACC_PRIVATE public cn.hdj.jvm.bytecode.SynchronizedInstruction(); flags: ACC_PUBLIC Code: stack=3, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: new #2 // class java/lang/Object 8: dup 9: invokespecial #1 // Method java/lang/Object."<init>":()V 12: putfield #3 // Field lock:Ljava/lang/Object; 15: return LineNumberTable: line 8: 0 line 9: 4 void onlyMe(java.lang.Object); flags: Code: stack=2, locals=4, args_size=2 0: aload_1 //將lock對象入棧 1: dup //復制棧頂元素 2: astore_2 //將棧頂元素存儲到局部變量表Slot2中 3: monitorenter //以lock對象為鎖,開始同步 4: aload_2 //將局部變量表Slot2中元素入棧 5: monitorexit //退出同步 6: goto 14 //程序正常結束,跳轉到14返回 9: astore_3 //從這步開始是異常路徑,開下面的Exception table 10: aload_2 //將局部變量表Slot2中元素入棧 11: monitorexit //退出同步 12: aload_3 //將局部變量表Slot3中元素(異常對象)入棧 13: athrow //把異常對象重新拋出個onlyMe方法調用者 14: return //方法返回 Exception table: from to target type 4 6 9 any 9 12 9 any LineNumberTable: line 11: 0 line 13: 4 line 14: 14 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 9 locals = [ class cn/hdj/jvm/bytecode/SynchronizedInstruction, class java/lang/Object, class java/lang/Object ] stack = [ class java/lang/Throwable ] frame_type = 250 /* chop */ offset_delta = 4 }
三、例子解析
- 代碼
public class DemoDynamic {
public static void foo() {
int a = 1;
int b = 2;
int c = (a + b) * 5;
}
}
-
javap 命令(也可以使用 IDEA 查看字節碼工具:jclasslib)
javac -g -encoding utf-8 DemoDynamic.java javap -verbose -c .\DemoDynamic.class > .\DemoDynamic.javap
-
字節文件
Classfile /D:/IDEA/Java-Learning/src/main/java/cn/hdj/jvm/bytecode/DemoDynamic.class Last modified 2020-10-17; size 419 bytes MD5 checksum 0242e2d86e94eb62d302f5a034336416 Compiled from "DemoDynamic.java" public class cn.hdj.jvm.bytecode.DemoDynamic minor version: 0 //版本號 major version: 52 flags: ACC_PUBLIC, ACC_SUPER //訪問標識符 Constant pool: //常量池 #1 = Methodref #3.#18 // java/lang/Object."<init>":()V #2 = Class #19 // cn/hdj/jvm/bytecode/DemoDynamic #3 = Class #20 // java/lang/Object #4 = Utf8 <init> #5 = Utf8 ()V #6 = Utf8 Code #7 = Utf8 LineNumberTable #8 = Utf8 LocalVariableTable #9 = Utf8 this #10 = Utf8 Lcn/hdj/jvm/bytecode/DemoDynamic; #11 = Utf8 foo #12 = Utf8 a #13 = Utf8 I #14 = Utf8 b #15 = Utf8 c #16 = Utf8 SourceFile #17 = Utf8 DemoDynamic.java #18 = NameAndType #4:#5 // "<init>":()V #19 = Utf8 cn/hdj/jvm/bytecode/DemoDynamic #20 = Utf8 java/lang/Object { public cn.hdj.jvm.bytecode.DemoDynamic(); //默認的構造方法 descriptor: ()V flags: ACC_PUBLIC Code: //棧容量1 , 局部變量表容量1, 參數個數1(因為每個實例方法都會有一個隱藏參數this) stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 6: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcn/hdj/jvm/bytecode/DemoDynamic; public static void foo(); //foo() 方法 descriptor: ()V flags: ACC_PUBLIC, ACC_STATIC //標識符,public static Code: //方法表中Code 屬性 stack=2, locals=3, args_size=0 //棧容量2 , 局部變量表容量3, 參數個數0 0: iconst_1 // 將常量值1入棧-> 棧1=1 1: istore_0 // 將棧頂元素存儲到局部變量表Slot1位置 -> 局部0=1 2: iconst_2 // 將常量值2入棧 -> 棧1=2 3: istore_1 // 將棧頂元素存儲到局部變量表Slot2位置 -> 局部1=2 4: iload_0 // 將局部變量表Slot1中元素入棧 5: iload_1 // 將局部變量表Slot2中元素入棧 6: iadd // 執行相加操作, 1+2 = 3, 入棧 7: iconst_5 // 將常量值5入棧 8: imul // 執行相乘操作,3*5=15,入棧 9: istore_2 // 將棧頂元素存儲到局部變量表Slot2位置-> 局部2=15 10: return //返回 LineNumberTable: //行數表 line 9: 0 line 10: 2 line 11: 4 line 12: 10 LocalVariableTable: //局部變量表 Start Length Slot Name Signature 2 9 0 a I 4 7 1 b I 10 1 2 c I } SourceFile: "DemoDynamic.java"
1602932223498-945875c1-116c-425e-9ba7-17ff982d10e2
四、字節碼增強
具體詳情看 字節碼增強技術探索,這里只簡單列出相關工具及使用場景。
4.1 ASM
對于需要手動操縱字節碼的需求,可以使用 ASM,它可以直接生產 .class 字節碼文件,也可以在類被加載入 JVM 之前動態修改類行為
- ASM 工具 輔助工具
- IDEA 插件 ASM ByteCode Outline,用于查看類中的代碼對應的 ASM 寫法
4.2 Javassist
利用 Javassist 實現字節碼增強時,可以無須關注字節碼刻板的結構,其優點就在于編程簡單。直接使用 java 編碼的形式,而不需要了解虛擬機指令,就能動態改變類的結構或者動態生成類。
4.3 Instrument
instrument 是 JVM 提供的一個可以修改已加載類的類庫,專門為 Java 語言編寫的插樁服務提供支持。它需要依賴 JVMTI 的 Attach API 機制實現。注意:ASM 和 Javassist 操作字節碼庫只能在類加載前對類進行強化。
4.5 字節碼增強技術使用場景
AOP 面向切面編程
熱部署:不部署服務而對線上服務做修改,可以做打點、增加日志等操作。
Mock:測試時候對某些服務做 Mock。
性能診斷工具:比如 bTrace 就是利用 Instrument,實現無侵入地跟蹤一個正在運行的 JVM,監控到類和方法級別的狀態信息。
參考
- 《深入了解 Java 虛擬機 Java 高級特性和最佳實踐》
- https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html
- 虛擬機指令 https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5
- https://www.cnblogs.com/noteless/p/9556928.html
- 字節碼增強技術探索 https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html
- 輕松看懂 Java 字節碼 https://juejin.im/post/6844903588716609543