深入理解JVM字节码

1 深入剖析class文件结构

1.1 初探class文件

在IDEA中编写代码并运行后,就会自动在out目录下生成对应的class文件

1
2
3
4
5
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, World");
}
}

用010Editor查看class文件

image-20211229095649390

1.2 class文件结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
classFile {
u4 magic; // 魔数
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.2.1 魔数

可以从上文看到Hello.class的魔数是四字节的0xCAFEBABE(咖啡宝贝?)

魔数是JVM识别class文件的标志,虚拟机在加载类文件之前会检查魔数是否正确,否则抛出java.lang.ClassFormatError异常,比如将魔数改为0xCAFEBABB

image-20211229102108725

然后在终端运行修改过的Hello.class,(注意如果直接IDEA运行会重新生成class文件并覆盖旧的)

1
java Hello

image-20211229102509877

1.2.2 版本号

0x0034表示主版本号为52(0x34),虚拟机解析时可以知道这是一个Java 8编译出的类。如果类文件版本高于JVM版本,加载类时会抛出异常java.lang.UnsupportedClassVersionError


关于版本号

每次Java发布大版本,主版本号就会加1,比如Java 8是52,Java 9是53


我们把主版本号改成53(0x35),然后终端运行一下修改后的类文件

image-20211229103408691

1.2.3 常量池

一、常量池是做什么用的?

像0这样的简单常用的操作数,会被内嵌到字节码中。如果操作数比较大或者是字符串常量,class文件就会将其存储在常量池,使用时,根据索引位置来查找。

二、常量池大小

比如大小为n时,真正有效的索引是1~n-1,因为0属于保留索引。

三、常量池项(cp_info)集合

最多包含n-1(因为long和double类型的常量会占用两个索引位置)

四、每个常量项——cp_info的结构
1
2
3
4
cp_info {
u1 tag; // 类型
u1 info[];
}
五、常量池类型
类型 tag值
CONSTANT_Utf8_info 1
CONSTANT_Integer_info 3
CONSTANT_Flaot_info 4
CONSTANT_Long_info 5
CONSTANT_Double_info 6
CONSTANT_Class_info 7
CONSTANT_String_info 8
CONSTANT_Fieldref_info 9
CONSTANT_Methodref_info 10
CONSTANT_InterfaceMethodref_info 11
CONSTANT_NameAndType_info 12
CONSTANT_MethodHandle_info 15
CONSTANT_MethodType_info 16
CONSTANT_InvokeDynamic_info 18

(1)查看类文件Hello.class常量池的方法

1
javap -v Hello

image-20211229152410890

六、CONSTANT_Integer_info和CONSTANT_Flaot_info

这两种常量都是4字节的。其结构分别如下

1
2
3
4
CONSTANT_Integer_info {
u1 tag;
u4 bytes;
}
1
2
3
4
CONSTANT_Float_info {
u1 tag;
u4 bytes;
}

其他如boolean、byte、short、char类型的变量,在常量池中都被当做int来处理。

有示例类MyConstantTest如下

1
2
3
4
5
6
7
public class MyConstantTest {
public final boolean bool = true;
public final char c = 'A';
public final byte b =66;
public final short s = 67;
public final int i = 68;
}

在终端中javac MyConstantTest.java生成class文件,然后拖入010Editor查看。

image-20211229160807279

七、CONSTANT_Long_info和CONSTANT_Double_info

这两个都是8字节。结构如下

1
CONSTANT_Long_info {    u1 tag;    u4 high_bytes;    u4 low_bytes;}
1
CONSTANT_Double_info {    u1 tag;    u4 high_bytes;    u4 low_bytes;}

示例程序如下

1
public class HelloWorldMain {    public final long a = Long.MAX_VALUE;}

image-20211229161606371

八、CONSTANT_Utf8_info

存储字符串内容,其结构如下

1
CONSTANT_Utf8_info {    u1 tag;    u2 length; // 注意,这个length不是表示字符串有多少字符,而是表示byte数组的长度    u1 bytes[length];  // 采用MUTF-8编码的字节数组}

什么是MUTF-8编码

这要先从UTF-8编码说起。UTF-8是一种变长编码,使用1~4字节表示一个字符。规则如下:

参考 - 知乎 - Unicode 和 UTF-8 有什么区别?

Unicode范围(十六进制) UTF-8编码方式(二进制)
0000 0001 - 0000 007F 0xxxxxxx
0000 0080 - 0000 07FF 110xxxxx 10xxxxxx
0000 0800 - 0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx
0001 0000 - 0010 FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

比如A的Unicode值是0x41在0000 0001 - 0000 007F范围内,将0x41填到7个x中,所以其UTF-8值为01000001,即0x41

再如Unicode是0x4E25,其范围在0000 0800 - 0000 FFFF,将0x4E25从低位开始填入1110xxxx 10xxxxxx 10xxxxxx的16个x中,得到其UTF-8为11100100 10111000 10100101,也即0xE4B8A5

MUTF-8的不同指出在于:

(1)MUTF-8中用两个字节表示空字符("\0"):把110xxxxx 10xxxxxx中的x全部填入0,也就是0xC080。而UTF-8只用一个字节,也就是0x00表示。MUTF-8这种方式的好处在于可以保证字符串中不会出现空字符,在C语言处理时不会出现意外截断。

(2)MUTF-8只用了标准UTF-8中的三种编码方式:单字节、两字节、三字节,没有用到四字节。需要编码超过0000 FFFF的字符时,Java使用“代理对”通过两个字符来表示。


1
2
3
4
public class HelloWorldMain {
public final String x = "\0";
public final String y = "\uD83D\uDE02";
}

y是笑哭的emoji

将其编译为字节码后,先用javap -v HelloWorldMain.class查看常量池

image-20211230095140759

拖到010Editor查看

image-20211230095331309

可以看到x对应的常量池字节信息第一个字节为tag,值为1也就是对应着CONSTANT_Utf8_info,然后紧跟的两个字节表示字节数组长度为2,后面的0xC080就是x的值了。

y的解码过程

y的tag值也是一样,其字节数组长度为6,也就是用6个字节来表示,前三个字节ED A0 BD对应二进制为11101101 10100000 10111101,根据UTF-8三字节的编码方法,解码时去掉第一个字节的110和第二三字节的10,剩下1101 1000 0011 1101也即0xD83D,同理后三字节ED B8 82可以解码为0xDE02,因此得到这个emoji的编码为4字节的0xD83DDE02

九、CONSTANT_String_info

与CONSTANT_Utf8_info的区别在于,CONSTANT_Utf8_info存储了字符串真正的内容,而CONSTANT_String_info存的是指向CONSTANT_Utf8_info常量类型的索引。其结构为

1
2
3
4
CONSTANT_String_info {
u1 tag;
u2 string_index;
}

示例

1
2
3
public class Hello {
private String a = "hello";
}

image-20211230103437297

在常量池第15个常量,存放了字符串的内容。

十、CONSTANT_Class_info

用来表示类或接口。结构与CONSTANT_String_info类似

1
2
3
4
CONSTANT_Class_info {
u1 tag;
u2 name_index; // 指向CONSTANT_Utf8_info常量,存放类或接口的全限定名(带包路径的)
}

image-20211230104841133

十一、CONSTANT_NameAndType_info

表示字段或者方法

1
2
3
4
5
CONSTANT_NameAndType_info {
u1 tag;
u2 name_index;
u2 descriptor_index;
}

name_index和descriptor_index都指向CONSTANT_Utf8_info,前者表示字段或方法名,后者是字段或方法描述符,用来表示字段或方法的类型。

1
public class Hello {    public void testMethod(int id, String name) {    }}
这里有个疑问?

按《深入理解JVM字节码》P15所述,name_index和descriptor_index分别指向第4和第5个位置,也就应该对应到constant_pool[3]constant_pool[4]才对,但实际上对应的是constant_pool[7]constant_pool[8]

image-20211230111445923

十二、CONSTANT_Fieldref_info、CONSTANT_Methodref_info和CONSTANT_InterfaceMethodref_info

结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CONSTANT_Fieldref_info {
u1 tag; // 10
u2 class_index; // 指向CONSTANT_Class_info的常量池索引值
u2 name_and_type_index; // 指向CONSTANT_NameAndType_info的常量池索引值
}

CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}

CONSTANT_InterfaceMethodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}

示例代码

1
2
3
4
5
6
7
public class HelloWorldMain {
public static void main(String[] args) {
new HelloWorldMain().testMethod(1, "hi");
}
public void testMethod(int id, String name) {
}
}

编译得到类文件,查看其常量池javap -v HelloWorldMain.class

image-20211230145707468

(1)所以,类名的索引在constant_pool[1]

image-20211230145939579

全限定名在constant_pool[26]

image-20211230150029696

(2)方法相关信息在constant_pool[4]

image-20211230150219157

所以,类索引是2,也就是上文对应的constant_pool[1],方法名和类型的常量池索引为29,也即在constant_pool[28]

image-20211230150431943

所以方法名在constant_pool[17],方法类型constant_pool[18],如下

image-20211230150550193

image-20211230150607696

十三、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info

从JDK1.7后为更好地支持动态语言调用,增加的3中常量池。CONSTANT_InvokeDynamic_info的作用是为invokedynamic指令提供启动引导方法。

1
2
3
4
5
CONSTANT_InvokeDynamic_info {
u1 tag;
u2 bootstrap_method_attr_index; // 指向bootstrap_methods[]的索引
u2 name_and_type_index; // 指向CONSTANT_NameAndType_info的索引
}

1.2.4 Access flags

紧跟在常量池后的两个字节,共16个标记位,目前只使用了8个,完整的访问标记含义如图

image-20211230152456904

如下0x21就意味着有ACC_SUPER和ACC_PUBLIC标记

image-20211230151938023

1.2.5 字段表

一、field_info结构
1
2
3
4
5
6
7
field_info {
u2 access_flags; // 字段访问标记
u2 name_index; // 字段名的常量池索引
u2 descriptor_index; // 字段描述符的索引
u2 attributes_count; // 属性个数
attribute_info attributes[attributes_count]; // 属性集合
}
二、字段访问标记

image-20211230153542867

image-20211230153555318

三、字段描述符

表示某个field的类型

image-20211230153837403

说明:

(1)引用类型后面的分号是为了防止多个连续引用类型描述符出现混淆。比如字符串类型String的描述符为Ljava/lang/String;

(2)JVM用前置[表示数组,比如int[]的类型描述符为[I,字符串数组String[]的描述符为[Ljava/lang/String;。多维数组就是多加几个[,比如Object[][][]描述符就是[[[Ljava/lang/Object;

1.2.6 方法表

一、method_info结构
1
2
3
4
5
6
7
method_info {
u2 access_flags; // 方法访问标记
u2 name_index; // 方法名的常量池索引
u2 descriptor_index; // 方法描述符的索引
u2 attributes_count; // 属性个数
attribute_info attributes[attributes_count]; // 属性集合
}
二、方法访问标记

image-20211230154529081

三、方法描述符

格式如下

1
(参数1类型 参数2类型 参数3类型 ...) 返回值类型 

比如Object foo(int i, double d, Thread t)的描述符为(IDLjava/lang/Thread;)Ljava/lang/Object;

1.2.7 方法属性表

一、属性表结构
1
2
3
4
{
u2 attribute_count;
attribute_info attributes[attribute_count];
}

其中每个属性项的结构如下

1
2
3
4
5
attribute_info {
u2 attribute_name_index; // 属性名对应的常量池索引
u4 attribute_length; // info数组的长度
u1 info[attribute_length]; // 具体内容
}
二、ConstantValue属性

用来表示静态变量初始值,结构如下

1
2
3
4
5
ConstantValue_attribute {
u2 attribute_name_index; // 指向常量池中值为"ConstantValue"的字符串常量的索引
u4 attribute_length; // 为固定值2
u2 constantvalue_index; // 根据具体变量的不同,指向具体的常量值索引。
}

比如变量为long类型,constantvalue_index就指向CONSTANT_Long_info类型的常量项

三、Code属性

除native和abstract方法以外,每个方法都有且仅有一个Code属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Code_attribute {
u2 attribute_name_index; // 属性名索引,2字节,指向CONSTANT_Utf8_info,这里对应"Code"的索引
u4 attribute_length; // 2字节,属性值长度
u2 max_stack; // 操作栈最大深度
u2 max_locals; // 局部变量表大小
u4 code_length; // 字节码指令长度,占4字节
u1 code[code_length]; // 存储字节码指令的字节数组
u2 exception_table_length; // 代码内部异常表的长度
{
u2 start_pc; // 异常处理器覆盖的字节码开始位置
u2 end_pc; // 异常处理器覆盖的字节码结束位置,左闭右开
u2 handler_pc; // 表示异常处理handler在code字节数组中的起始位置
u2 catch_type; // 表示需要处理的catch的异常类型是什么,2字节表示,指向CONSTANT_Class_info
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}

Java虚拟机规定Code属性只能包含四种可选属性:LineNumberTable、LocalVariableTable、LocalVariableTypeTable、StackMapTable。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.io.IOException;

public class HelloWorldMain {
public HelloWorldMain() {
}

public static void main(String[] args) {
try {
foo();
} catch (NullPointerException var3) {
System.out.println(var3);
} catch (IOException var4) {
System.out.println(var4);
}

try {
foo();
} catch (Exception var2) {
System.out.println(var2);
}

}

public static void foo() throws IOException {
}
}

image-20211230162714138

1.3 javap

javap [options] <classes>

(1)javap -p显示private方法和字段

(2)javap -s可以输出类型描述符签名信息

(3)javap -c可以对类文件进行反编译,可以显示出方法内的字节码

(4)javap -v可以显示更加详细的内容,比如版本号、类访问权限、常量池等信息

(5)javap -l可以显示行号表和局部变量表。不过一般只显示行号表。因为想要显示局部变量表,需要在javac编译时加上-g选项。

2 字节码基础

6 ASM和Javassit字节码操作工具

6.1 ASM Core API核心类

(1)ClassVisitor:是一个抽象类

(2)ClassWriter:ClassVisitor的实现类,在visit方法中可以修改原始字节码;toByteArray方法可以返回修改后的字节数组

(3)ClassReader:负责解析class文件。accept调用后,ClassReader会把解析Class文件过程中的事件源源不断地通知给ClassVisitor对象调用不同的visit方法,ClassVisitor可以在这些visit方法中对字节码进行修改,ClassWriter可以生成最终修改过的字节码。解析过程中遇到不同的节点会调用不同的visit方法。ASM的visit方法调用时序如下:

image-20220104111355371

6.2 如何使用ASM API操作字节码

一、访问类的方法和字段

(1)新建类

1
2
3
4
5
6
public class MyMain {
public int a = 0;
public int b = 1;
public void test01(){}
public void test02(){}
}

(2)编译,生成MyMain.class文件

(3)使用ASM输出类的方法和字段列表

读取文件字节数组的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import jdk.internal.org.objectweb.asm.*;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;

import static jdk.internal.org.objectweb.asm.Opcodes.ASM5;

public class ASMOut {
public static void main(String[] args) throws IOException {
byte[] bytes = Files.readAllBytes(new File("F:\\java\\ASMTest\\src\\MyMain.class").toPath());
ClassReader cr = new ClassReader(bytes);
ClassWriter cw = new ClassWriter(0);
ClassVisitor cv = new ClassVisitor(ASM5, cw) {
@Override
public FieldVisitor visitField(int i, String s, String s1, String s2, Object o) {
System.out.println("field: " + s);
return super.visitField(i, s, s1, s2, o);
}

@Override
public MethodVisitor visitMethod(int i, String s, String s1, String s2, String[] strings) {
System.out.println("method: " + s);
return super.visitMethod(i, s, s1, s2, strings);
}
};
cr.accept(cv, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);
}
}

输出结果如下:

image-20220104111908725

accept方法的第二个参数:位掩码
位掩码 含义
SKIP_DEBUG 跳过类文件中的调试信息
SKIP_CODE 跳过方法体中的Code属性
EXPAND_FRAMES 展开StackMapTable属性
SKIP_FRAMES 跳过StackMapTable属性

二、新增字段

利用visitEnd方法添加字段

这里给MyMain新增了一个String类型的xyz字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import jdk.internal.org.objectweb.asm.*;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

import static jdk.internal.org.objectweb.asm.Opcodes.ASM5;

public class ASMOut {
public static void main(String[] args) throws IOException {
byte[] bytes = Files.readAllBytes(new File("F:\\java\\ASMTest\\src\\MyMain.class").toPath());
ClassReader cr = new ClassReader(bytes);
ClassWriter cw = new ClassWriter(0);
ClassVisitor cv = new ClassVisitor(ASM5, cw) {
@Override
public void visitEnd() {
super.visitEnd();
FieldVisitor fv = cv.visitField(Opcodes.ACC_PUBLIC, "xyz", "Ljava/lang/String;", null, null);
if (fv != null) fv.visitEnd();
}
};
cr.accept(cv, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);
byte[] bytesModified = cw.toByteArray();
Files.write(Paths.get("F:\\java\\ASMTest\\src\\MyMain2.class"), bytesModified);
}
}

javap查看MyMain2的字节码,可以看到多出的String类型的xyz变量,部分字节码如下:

image-20220104113846592

三、新增方法

这里新增一个xyz方法,其签名为(ILjava/lang/String;)V(前文笔记中已有“方法描述符”的格式参考)。

新增方法与新增字段的区别在于visitEnd里是MethodVisitor还是FieldVisitor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import jdk.internal.org.objectweb.asm.*;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

import static jdk.internal.org.objectweb.asm.Opcodes.ASM5;

public class ASMOut {
public static void main(String[] args) throws IOException {
byte[] bytes = Files.readAllBytes(new File("F:\\java\\ASMTest\\src\\MyMain.class").toPath());
ClassReader cr = new ClassReader(bytes);
ClassWriter cw = new ClassWriter(0);
ClassVisitor cv = new ClassVisitor(ASM5, cw) {
@Override
public void visitEnd() {
super.visitEnd();
MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC, "xyz", "(ILjava/lang/String;)V", null, null);
if (mv != null) mv.visitEnd();
}
};
cr.accept(cv, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);
byte[] bytesModified = cw.toByteArray();
Files.write(Paths.get("F:\\java\\ASMTest\\src\\MyMain2.class"), bytesModified);
}
}

javap查看MyMain2的字节码

image-20220104142030332

四、移除方法和字段

a. 移除的本质在于

将visit方法(visitField和visitMethod)返回null

b. 示例

以下MyMain类为例,删除abc字段和xyz方法。

1
2
3
4
5
6
7
8
public class MyMain {
private int abc = 0;
private int def = 0;
public void foo() {}
public int xyz(int a, String b) {
return 0;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import jdk.internal.org.objectweb.asm.*;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

import static jdk.internal.org.objectweb.asm.Opcodes.ASM5;

public class ASMOut {
public static void main(String[] args) throws IOException {
byte[] bytes = Files.readAllBytes(new File("F:\\java\\ASMTest\\src\\MyMain.class").toPath());
ClassReader cr = new ClassReader(bytes);
ClassWriter cw = new ClassWriter(0);
ClassVisitor cv = new ClassVisitor(ASM5, cw) {
@Override
public FieldVisitor visitField(int i, String s, String s1, String s2, Object o) {
if ("abc".equals(s)) {
return null;
}
return super.visitField(i, s, s1, s2, o);
}

@Override
public MethodVisitor visitMethod(int i, String s, String s1, String s2, String[] strings) {
if ("xyz".equals(s)) {
return null;
}
return super.visitMethod(i, s, s1, s2, strings);
}
};
cr.accept(cv, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);
byte[] bytesModified = cw.toByteArray();
Files.write(Paths.get("F:\\java\\ASMTest\\src\\MyMain2.class"), bytesModified);
}
}

使用javap查看MyMain2的字节码发现只剩下def字段和foo方法了

五、修改方法内容

修改的本质

移除+新增

示例
1
2
3
4
5
6
7
8
public class MyMain {
public static void main(String[] args) {
System.out.println(new MyMain().foo(1));
}
public int foo(int a) {
return a; // 修改为 return a+100;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import jdk.internal.org.objectweb.asm.*;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

import static jdk.internal.org.objectweb.asm.Opcodes.ASM5;

public class ASMOut {
public static void main(String[] args) throws IOException {
byte[] bytes = Files.readAllBytes(new File("F:\\java\\ASMTest\\src\\MyMain.class").toPath());
ClassReader cr = new ClassReader(bytes);
ClassWriter cw = new ClassWriter(0);
ClassVisitor cv = new ClassVisitor(ASM5, cw) {
@Override
public MethodVisitor visitMethod(int i, String s, String s1, String s2, String[] strings) {
if ("foo".equals(s)) {
return null;
}
return super.visitMethod(i, s, s1, s2, strings);
}

@Override
public void visitEnd() {
super.visitEnd();
MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC, "foo", "(I)I", null, null);
mv.visitCode();
mv.visitVarInsn(Opcodes.ILOAD, 1);
mv.visitIntInsn(Opcodes.BIPUSH, 100);
mv.visitInsn(Opcodes.IADD);
mv.visitInsn(Opcodes.IRETURN);
mv.visitEnd();
}
};
cr.accept(cv, 0);
byte[] bytesModified = cw.toByteArray();
Files.write(Paths.get("F:\\java\\ASMTest\\src\\MyMain.class"), bytesModified);
}
}

image-20220104145839022

ClassFormatError异常

执行java -cp . MyMain报错,提示入参无法放到局部变量表。

image-20220104150249362

解决方法:让ASM自动计算stack和locals。这与ClassWriter构造器方法参数有关。

构造器参数 含义
new ClassWriter(0) 不自动计算操作数栈和局部变量大小,需要手动指定
new ClassWriter(ClassWriter.COMPUTE_MAXS) 自动计算操作数栈和局部变量大小,前提是需要调用visitMaxs方法触发计算上述两个值,参数可以随意指定
new ClassWriter(ClassWriter.COMPUTE_FRAMES) 除了操作数栈和局部变量大小,还会自动计算StackMapFrames

修改代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import jdk.internal.org.objectweb.asm.*;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

import static jdk.internal.org.objectweb.asm.Opcodes.ASM5;

public class ASMOut {
public static void main(String[] args) throws IOException {
byte[] bytes = Files.readAllBytes(new File("F:\\java\\ASMTest\\src\\MyMain.class").toPath());
ClassReader cr = new ClassReader(bytes);
// 指定ClassWriter自动计算
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
ClassVisitor cv = new ClassVisitor(ASM5, cw) {
@Override
public MethodVisitor visitMethod(int i, String s, String s1, String s2, String[] strings) {
if ("foo".equals(s)) {
return null;
}
return super.visitMethod(i, s, s1, s2, strings);
}

@Override
public void visitEnd() {
super.visitEnd();
MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC, "foo", "(I)I", null, null);
mv.visitCode();
mv.visitVarInsn(Opcodes.ILOAD, 1);
mv.visitIntInsn(Opcodes.BIPUSH, 100);
mv.visitInsn(Opcodes.IADD);
mv.visitInsn(Opcodes.IRETURN);
// 触发计算
mv.visitMaxs(0,0);
mv.visitEnd();
}
};
cr.accept(cv, 0);
byte[] bytesModified = cw.toByteArray();
Files.write(Paths.get("F:\\java\\ASMTest\\src\\MyMain.class"), bytesModified);
}
}

再次执行MyMain,正常运行。

image-20220104151054651

六、AdviceAdapter使用

作用

在方法的开始和结束插入代码

示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import jdk.internal.org.objectweb.asm.*;
import jdk.internal.org.objectweb.asm.commons.AdviceAdapter;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

import static jdk.internal.org.objectweb.asm.Opcodes.ASM5;

public class ASMOut {
public static void main(String[] args) throws IOException {
byte[] bytes = Files.readAllBytes(new File("F:\\java\\ASMTest\\src\\MyMain.class").toPath());
ClassReader cr = new ClassReader(bytes);
// 指定ClassWriter自动计算
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
ClassVisitor cv = new ClassVisitor(ASM5, cw) {
@Override
public MethodVisitor visitMethod(int i, String s, String s1, String s2, String[] strings) {
MethodVisitor mv = super.visitMethod(i, s, s1, s2, strings);
if (!"foo".equals(s)) return mv;
return new AdviceAdapter(ASM5, mv, i, s, s1) {
@Override
protected void onMethodEnter() {
// 新增 System.out.println("enter " + s);
super.onMethodEnter();
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("enter " + s);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}

@Override
protected void onMethodExit(int i) {
// 新增 System.out.println("[normal, err] exit " + s);
super.onMethodExit(i);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
if (i == Opcodes.ATHROW) {
mv.visitLdcInsn("err exit " + s);
} else {
mv.visitLdcInsn("normal exit " + s);
}
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
};
}
};
cr.accept(cv, 0);
byte[] bytesModified = cw.toByteArray();
Files.write(Paths.get("F:\\java\\ASMTest\\src\\MyMain.class"), bytesModified);
}
}

image-20220104152825993

七、给方法加上try catch

7 Java Instrumentation原理

7.1 Java Instrumentation简介

JDK 1.5版本后引入java.lang.instrument包,可以实现字节码增强,其核心功能由java.lang.instrument.Instrumentation提供,该接口的方法提供了注册类文件转换器、获取所有已加载的类等功能,允许我们修改已加载或未加载的类,实现AOP、性能监控等功能。

一、修改字节码的基本过程

Instrumentation的常用组成

Instrumentation接口的addTransformer方法给Instrumentation注册一个类型为ClassFileTransformer的类文件转换器,ClassFileTransformer接口只有一个transform方法。

修改字节码的过程

调用addTransformer注册transform以后,后续所有JVM加载类都会被transform方法拦截,这个方法接受了原类文件的字节数组,在这个方法中可以做任意修改,最后返回转换过的字节数组,由JVM加载这个修改后的类文件。

如果transform方法返回null,则表示不做处理;如果返回值不为null,则说明,JVM会用返回的字节数组替换原来类的字节数组。

接口的其他方法
1
2
3
retransformClasses				// 对JVM已加载的类重新触发类加载
getAllLoadedClasses // 获取当前JVM加载的所有类对象
isRetransformClassesSupported // 当前JVM配置是否支持类重新转换的特性
Instrumentation的两种使用方法

(1)在JVM启动时添加一个Agent的jar包

(2)JVM运行以后在任意时刻通过Attach API远程加载Agent的jar包



----------- 本文结束 -----------




0%