一、概念
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"
四、字节码增强
具体详情看 字节码增强技术探索,这里只简单列出相关工具及使用场景。
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