一、概念
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 文件的资源仓库,
主要存放:
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) 包括类变量和实例变量,但不包括方法内部声明的局部变量。
-
简单名称:没有类型和参数修饰的方法或者字段名称,如 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/String |
V |
void |
特殊类型 Void |
描述符来描述方法时,按照先参数列表,后返回值的顺序描述;如:java.lang.String.toString() 描述为 () Ljava/lang/String,java.lang.String#valueOf(char[], int, int) 描述为 ([CII)Ljava/lang/String
1.10 方法表集合
方法描述采取与字段描述完全一致的方式。
1.11 属性表集合在
属性表(attribute_info)在 Class 文件、字段表、方法表中都可以携带自己的属性表集合,用于描述某些场景专有的信息。
-
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 运算指令
相关指令
注意
- 只有当除法指令和求余指令遇到除数为零时,虚拟机会抛出 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 操作数栈的管理指令
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 之前动态修改类行为

4.2 Javassist
利用 Javassist 实现字节码增强时,可以无须关注字节码刻板的结构,其优点就在于编程简单。直接使用 java 编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构或者动态生成类。
4.3 Instrument
instrument 是 JVM 提供的一个可以修改已加载类的类库,专门为 Java 语言编写的插桩服务提供支持。它需要依赖 JVMTI 的 Attach API 机制实现。注意:ASM 和 Javassist 操作字节码库只能在类加载前对类进行强化。
4.5 字节码增强技术使用场景
参考