![图片[1]-37 JVM开发必备:三款核心字节码工具完全指南-速优课](http://www.suyouke.com/wp-content/uploads/2026/07/doubao_img_2304x1728_20260722_174942-1-1024x768.png)
本文导读
学习 JVM 的过程中,我们经常需要和字节码打交道:查看字节码、修改字节码、分析对象内存布局…… 工欲善其事,必先利其器。掌握好相关工具,能让你的 JVM 学习之路事半功倍。
本文将系统介绍三款 JVM 开发中最常用的工具:
- javap:JDK 自带的字节码反汇编工具,快速查看 class 文件内容
- OpenJDK Code Tools:包含 ASMTools 和 JOL 等实用小工具
- ASM:功能强大的字节码分析与修改框架,众多开源项目的基石
通过阅读本文,你将了解:
- javap 的输出结构各部分分别代表什么含义
- 如何使用 ASMTools 手工修改字节码
- 如何用 JOL 查看对象的内存布局
- ASM 框架的核心设计思想和访问者模式
- 如何用 ASM 生成和修改 class 文件
一、javap:JDK 自带的字节码反汇编工具
1.1 javap 是什么
javap 是 JDK 自带的反汇编工具,可以把 class 文件反汇编成人类可读的格式。学习 JVM 的过程中,我们会频繁地用它来查看编译后的字节码。
先看一段示例代码:
public class Foo {
private int tryBlock;
private int catchBlock;
private int finallyBlock;
private int methodExit;
public void test() {
try {
tryBlock = 0;
} catch (Exception e) {
catchBlock = 1;
} finally {
finallyBlock = 2;
}
methodExit = 3;
}
}
编译后用 javap 查看:
$ javac Foo.java
$ javap -p -v Foo
这里用到了两个常用选项:
-p:打印私有字段和方法(默认只打印非私有的)-v:输出详细信息(如果只想看方法字节码,可以用-c替代-v)
javap -v 的输出内容很多,我们分块来解读。
1.2 基本信息区
最开头是 class 文件的基本信息:
Classfile ../Foo.class
Last modified ..; size 541 bytes
MD5 checksum 3828cdfbba56fea1da6c8d94fd13b20d
Compiled from "Foo.java"
public class Foo
minor version: 0
major version: 54
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #7 // Foo
super_class: #8 // java/lang/Object
interfaces: 0, fields: 4, methods: 2, attributes: 1
这里包含了:
- class 文件版本号:
minor version: 0, major version: 54。版本号和 Java 版本对应——Java 8 是 52,Java 10 是 54,Java 11 是 55。 - 访问标志:
flags: (0x0021) ACC_PUBLIC, ACC_SUPER,表示这个类是 public 的。 - 类名与父类:
this_class指向当前类,super_class指向父类。 - 计数信息:接口数、字段数、方法数、属性数。
注意:新版本的
javac编译的 class 文件不能在旧版本的 JRE 上运行,否则会抛出UnsupportedClassVersionError。
1.3 常量池
接下来是常量池(Constant Pool),用来存放各种常量和符号引用:
Constant pool:
#1 = Methodref #8.#23 // java/lang/Object."<init>":()V
#2 = Fieldref #7.#24 // Foo.tryBlock:I
...
#14 = Utf8 <init>
#15 = Utf8 ()V
...
#23 = NameAndType #14:#15 // "<init>":()V
...
#30 = Utf8 java/lang/Object
常量池的每一项都有一个索引(如 #1),并且可能引用其他常量池项。比如 #1 是一个方法引用,它引用了 #8(类名)和 #23(名称和类型),而 #23 又引用了 #14(方法名)和 #15(描述符)。
可以把它想象成一棵树结构,叶子节点是字符串常量:
![图片[2]-37 JVM开发必备:三款核心字节码工具完全指南-速优课](https://www.suyouke.com/wp-content/uploads/2026/07/image-46-1024x396.png)
1.4 字段区
字段区列举了类中的各个字段:
private int tryBlock;
descriptor: I
flags: (0x0002) ACC_PRIVATE
主要信息是字段的类型(descriptor)和访问权限(flags)。对于 final 修饰的静态字段,如果是基本类型或字符串类型,还会显示常量值。
1.5 方法区
方法区是最核心的部分,每个方法都包含最重要的代码区域(Code):
public void test();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: iconst_0
2: putfield #2 // Field tryBlock:I
5: aload_0
6: iconst_2
7: putfield #3 // Field finallyBlock:I
10: goto 35
13: astore_1
14: aload_0
15: iconst_1
16: putfield #5 // Field catchBlock:I
19: aload_0
20: iconst_2
21: putfield #3 // Field finallyBlock:I
24: goto 35
27: astore_2
28: aload_0
29: iconst_2
30: putfield #3 // Field finallyBlock:I
33: aload_2
34: athrow
35: aload_0
36: iconst_3
37: putfield #6 // Field methodExit:I
40: return
Exception table:
from to target type
0 5 13 Class java/lang/Exception
0 5 27 any
13 19 27 any
LineNumberTable:
line 9: 0
...
StackMapTable: number_of_entries = 3
...
代码区包含以下几部分:
1. 头部信息
stack=2:操作数栈的最大深度locals=3:局部变量表的大小(注意这是字节码层面的局部变量,不完全等同于 Java 源码中的局部变量)args_size=1:方法接收的参数个数
2. 字节码指令 每条指令前面的数字是字节码偏移量(Bytecode Index, BCI),用来定位指令。比如 10: goto 35 表示偏移量 10 处的 goto 指令,跳转到偏移量 35 处。
3. 异常表(Exception table) 记录了每个异常处理器的信息:
from和to:监控的代码范围target:异常处理器的起始位置type:捕获的异常类型,any表示任意异常
4. 行号表(LineNumberTable) Java 源码行号到字节码偏移量的映射。如果编译时加上 -g 参数,还会有局部变量表(LocalVariableTable),展示每个局部变量的名字、类型和作用域。这些都属于调试信息,JVM 不要求 class 文件必须包含。
5. 栈映射表(StackMapTable) 描述了字节码跳转后操作数栈的分布情况,主要用于类加载时的类型校验和即时编译,一般不需要深入了解。
二、OpenJDK Code Tools:实用小工具集
2.1 Code Tools 项目简介
OpenJDK 的 Code Tools 项目包含好几个实用的小工具。这里介绍其中两个最常用的:
- ASMTools:字节码汇编器/反汇编器
- JOL:对象内存布局分析工具
2.2 ASMTools:手工修改字节码
ASMTools 可以把 class 文件反汇编成文本格式(.jasm),修改后再汇编回 class 文件。这在我们需要生成一些 Java 编译器无法生成的特殊类时非常有用。
反汇编命令:
$ java -cp /path/to/asmtools.jar org.openjdk.asmtools.jdis.Main Foo.class > Foo.jasm
汇编命令:
$ java -cp /path/to/asmtools.jar org.openjdk.asmtools.jasm.Main Foo.jasm
它的输出格式和 javap 不太一样,主要用来做一些简单的字节码修改。
举个实战例子:我们知道 Java 编译器不允许把 int 值 2 赋给 boolean 变量,但在字节码层面其实是可以的(boolean 在字节码中就是用 int 实现的)。
用 ASMTools 就可以实现这个”黑魔法”:
- 先写一个正常的 Java 类并编译
- 用 ASMTools 反汇编成
.jasm文件 - 找到
boolean flag = true对应的字节码:iconst_1;
istore_1; - 把
iconst_1改成iconst_2 - 保存后再汇编回 class 文件
这样就得到了一个 flag 值为 2 的 boolean 变量,这是 Java 编译器永远不会生成的。
同样,我们也可以用 ASMTools 验证一些 JVM 规范中的结论。比如 class 文件允许存在参数类型相同但返回类型不同的方法(Java 语言层面不允许,但字节码层面允许)。验证方法就是反汇编后复制一个方法,修改它的描述符(返回类型),再汇编回去看看能不能正常加载运行。
2.3 JOL:查看对象内存布局
JOL(Java Object Layout)是另一个非常实用的工具,可以用来查看 Java 对象在内存中的具体布局。
常用的两条命令:
# 查看对象内部细节
$ java -jar /path/to/jol-cli-0.9-full.jar internals java.util.HashMap
# 估算对象大小
$ java -jar /path/to/jol-cli-0.9-full.jar estimates java.util.HashMap
在我们学习对象内存布局那篇文章时,JOL 是验证理论知识的绝佳工具。它可以清晰地展示对象头、实例数据、对齐填充各占多少字节,字段是怎么排列的,等等。
三、ASM:强大的字节码操作框架
3.1 ASM 是什么
ASM 是一个功能强大的字节码分析与修改框架,应用非常广泛:
- 语言编译器:Groovy、Kotlin 的编译器
- 代码覆盖率工具:Cobertura、JaCoCo
- 各种字节码注入的监控工具
- Java 8 的 Lambda 表达式适配器类也是用 ASM 动态生成的
ASM 既可以生成全新的 class 文件,也可以修改已有的 class 文件。
3.2 ASMifier:快速上手 ASM 的神器
ASM 提供了一个辅助类 ASMifier,它可以接收一个 class 文件,然后输出一段能生成该 class 文件原始字节数组的 Java 代码。对于想快速上手 ASM 的人来说,这是最好的学习工具——你想生成什么样的字节码,就先用 Java 写出来编译好,再用 ASMifier 转成 ASM 代码,然后对照着学。
举个例子:
$ echo '
public class Foo {
public static void main(String[] args) {
boolean flag = true;
if (flag) System.out.println("Hello, Java!");
if (flag == true) System.out.println("Hello, JVM!");
}
}' > Foo.java
$ javac Foo.java
$ java -cp /PATH/TO/asm-all-6.0_BETA.jar org.objectweb.asm.util.ASMifier Foo.class | tee FooDump.java
生成的 FooDump.java 大致是这样的:
public class FooDump implements Opcodes {
public static byte[] dump () throws Exception {
ClassWriter cw = new ClassWriter(0);
FieldVisitor fv;
MethodVisitor mv;
AnnotationVisitor av0;
cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "Foo", null, "java/lang/Object", null);
// ... 构造器方法等 ...
{
mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
mv.visitCode();
mv.visitInsn(ICONST_1);
mv.visitVarInsn(ISTORE, 1);
mv.visitVarInsn(ILOAD, 1);
// ... 更多指令 ...
mv.visitInsn(RETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();
}
// ...
return cw.toByteArray();
}
}
可以看到:
ClassWriter用来生成类- 调用
visitMethod会得到一个MethodVisitor - 用
MethodVisitor来生成具体的指令
这就是 ASM 的访问者模式。
3.3 修改字节码的原理
生成新的 class 文件比较简单,那修改已有的 class 文件呢?原理也很直观:
ASM 提供了 ClassReader,它会读取 class 文件的原始字节,然后转换成一系列访问请求(也就是调用各种 visit 方法)。
如果我们直接把 ClassReader 的访问请求转发给 ClassWriter,那就是一个简单的”读取-写出”过程,结果和原来一样:
public static void main(String[] args) throws Exception {
ClassReader cr = new ClassReader("Foo");
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
cr.accept(cw, ClassReader.SKIP_FRAMES);
Files.write(Paths.get("Foo.class"), cw.toByteArray());
}
如果想修改字节码,就在 ClassReader 和 ClassWriter 中间插入一个自定义的访问者。这个访问者把请求转发给 ClassWriter 之前,可以做各种修改:
- 新增:在转发的请求后面追加新的请求
- 删除:不转发某些请求
- 修改:忽略原请求,发出新的请求
整个过程就像一条流水线:
![图片[3]-37 JVM开发必备:三款核心字节码工具完全指南-速优课](https://www.suyouke.com/wp-content/uploads/2026/07/image-47-1024x343.png)
来看一个具体的例子——把 main 方法的内容替换成一句 System.out.println("Hello, World!");:
import java.nio.file.*;
import org.objectweb.asm.*;
public class ASMHelper implements Opcodes {
static class MyMethodVisitor extends MethodVisitor {
private MethodVisitor mv;
public MyMethodVisitor(int api, MethodVisitor mv) {
super(api, null);
this.mv = mv;
}
@Override
public void visitCode() {
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Hello, World!");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(2, 1);
mv.visitEnd();
}
}
static class MyClassVisitor extends ClassVisitor {
public MyClassVisitor(int api, ClassVisitor cv) {
super(api, cv);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
String[] exceptions) {
MethodVisitor visitor = super.visitMethod(access, name, descriptor, signature, exceptions);
if ("main".equals(name)) {
return new MyMethodVisitor(ASM6, visitor);
}
return visitor;
}
}
public static void main(String[] args) throws Exception {
ClassReader cr = new ClassReader("Foo");
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
ClassVisitor cv = new MyClassVisitor(ASM6, cw);
cr.accept(cv, ClassReader.SKIP_FRAMES);
Files.write(Paths.get("Foo.class"), cw.toByteArray());
}
}
这段代码的逻辑是:
MyClassVisitor截获对main方法的访问- 返回自定义的
MyMethodVisitor代替原来的MethodVisitor MyMethodVisitor忽略所有传入的指令请求,只在visitCode时生成新的指令
这就是 ASM 修改字节码的基本套路。掌握了这个模式,你就能实现各种复杂的字节码操作了。
总结与思考
本文要点回顾
- javap:JDK 自带的反汇编工具,是学习字节码的入门必备
- 输出包括:基本信息、常量池、字段、方法
- 方法区的 Code 属性包含字节码指令、异常表、行号表等
- 常用参数:
-p(显示私有成员)、-v(详细输出)、-c(仅输出字节码)
- OpenJDK Code Tools:
- ASMTools:字节码汇编/反汇编,适合做简单的字节码修改,生成 Java 编译器无法生成的类
- JOL:查看对象内存布局,验证对象头、字段排列、对齐填充等理论知识
- ASM 框架:功能最强大的字节码操作框架
- 基于访问者模式,通过
ClassReader读取、ClassVisitor转换、ClassWriter写出 ASMifier是学习 ASM 的神器,可以把现有 class 文件转成 ASM 代码- 修改字节码的核心是在 reader 和 writer 之间插入自定义访问者
- 基于访问者模式,通过
思考与实践
- 动手用
javap -v查看你自己项目中的某个类,尝试读懂它的常量池和方法字节码。 - 用 JOL 查看
java.lang.Object、java.lang.String、java.util.HashMap等常用类的内存布局,和你学到的理论对照一下。 - 试着用 ASM 实现一个简单的字节码注入:在某个方法的入口和出口分别打印一条日志。
工具是我们探索 JVM 世界的眼睛和手。熟练掌握这些工具,你就能更高效地学习 JVM,也能更从容地解决实际工作中的各种问题。








请登录后查看评论内容