JVM字节码与执行引擎

JVM字节码与执行引擎

JVM 内存布局是排查线上问题的基础。很多开发者遇到 OutOfMemoryError 时不知道从哪里入手。本文结合实际案例,讲清楚各区域的作用和常见异常,帮你建立排查思路。

字节码概览

Java 字节码是 JVM 的指令集,每条指令由一个字节的操作码(Opcode)和零到多个操作数组成。

#

查看字节码

# javap 反编译
javap -c -p HelloWorld.class

# 更详细的输出
javap -v HelloWorld.class

#

HelloWorld 字节码示例

public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello, World!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return

常见字节码指令

#

加载和存储

指令 说明
iload 将 int 局部变量压入操作数栈
istore 将栈顶 int 存入局部变量
ldc 将常量压入操作数栈
getstatic 获取静态字段
putfield 设置对象字段

#

算术运算

指令 说明
iadd int 加法
isub int 减法
imul int 乘法
idiv int 除法
iinc 局部变量自增

#

类型转换

指令 说明
i2l int 转 long
i2f int 转 float
l2i long 转 int
checkcast 类型检查转换

#

对象操作

指令 说明
new 创建对象
invokespecial 调用构造方法/私有方法/父类方法
invokevirtual 调用虚方法
invokestatic 调用静态方法
invokeinterface 调用接口方法
invokedynamic 动态调用(Java 7+ Lambda)

#

控制转移

指令 说明
ifeq/ifne 等于/不等于0跳转
if_icmplt int 比较小于跳转
goto 无条件跳转
tableswitch 表格开关
lookupswitch 查找开关

执行引擎

#

1. 解释执行(Interpreter)

字节码
|
v
解释器逐条读取指令
|
v
调用对应的C++函数执行
|
v
结果

特点:启动快,执行慢。

#

2. JIT 编译(Just-In-Time)

字节码
|
v
热点代码检测(方法调用次数 > 阈值)
|
v
编译为本地机器码
|
v
直接执行机器码

热点检测

-XX:CompileThreshold=10000  # 方法调用次数阈值(Client: 1500, Server: 10000)

#

3. 分层编译(Tiered Compilation)

JDK 7+ 默认开启:

Level 0: 解释执行
↓ 方法调用次数 > 阈值
Level 1: C1 简单编译(带性能监控)
↓ 代码成为热点
Level 2: C1 复杂编译
↓ 更热点
Level 3: C1 完全编译(带所有性能监控)
↓ 最热点
Level 4: C2 激进优化编译
-XX:+TieredCompilation  # 开启分层编译(默认开启)

C1 vs C2 编译器

特性 C1(Client Compiler) C2(Server Compiler)
优化程度 简单 激进
编译速度
生成代码质量 一般
启动速度
适用场景 客户端/短运行 服务端/长运行

JIT 优化技术

#

1. 方法内联

public int add(int a, int b) {
return a + b;
}

public int calculate() {
int x = add(1, 2); // 内联后:直接变成 int x = 1 + 2;
return x * 3;
}
-XX:MaxInlineSize=35       # 方法体小于35字节内联
-XX:FreqInlineSize=325 # 热点方法小于325字节内联

#

2. 逃逸分析

public void method() {
User user = new User(); // 未逃逸出方法
user.setName("test");
// JIT优化:栈上分配或标量替换
}
-XX:+DoEscapeAnalysis       # 开启逃逸分析
-XX:+EliminateAllocations # 标量替换
-XX:+EliminateLocks # 同步消除

#

3. 锁消除

public void method() {
Object lock = new Object();
synchronized (lock) { // 锁对象未逃逸,消除锁
// ...
}
}

#

4. 循环展开

// 原始代码
for (int i = 0; i < 100; i++) {
sum += array[i];
}

// 优化后
for (int i = 0; i < 100; i += 4) {
sum += array[i];
sum += array[i + 1];
sum += array[i + 2];
sum += array[i + 3];
}

查看 JIT 编译

# 打印 JIT 编译日志
-XX:+PrintCompilation

# 输出示例
123 45 4 java.lang.String::charAt (29 bytes)
│ │ │ └── 编译级别4(C2)
│ │ └── 方法编号
│ └── 编译ID
└── JVM启动后的毫秒数
# 打印更详细的信息
-XX:+UnlockDiagnosticVMOptions
-XX:+PrintInlining # 打印内联决策
-XX:+PrintAssembly # 打印汇编代码(需要hsdis)

AOT 编译(Ahead-Of-Time)

JDK 9+ 引入 jaotc:

# 编译为本地代码
jaotc --output libHello.so Hello.class

# JVM 加载
java -XX:AOTLibrary=./libHello.so Hello

特点

  • 启动即 peak performance
  • 减少 JIT 编译开销
  • 但失去跨平台性

总结

执行方式 启动速度 峰值性能 适用场景
解释执行 最快 最慢 极少
C1 编译 客户端
C2 编译 最高 服务端
分层编译 较快 最高 现代 JVM 默认
AOT 最快 容器/Serverless

JVM 的执行引擎设计精妙:解释执行快速启动,JIT 编译提升峰值性能,分层编译兼顾两者,AOT 为云原生场景提供新选择。

核心要点

  1. 堆内存分为年轻代和老年代,年轻代又分为 Eden、Survivor 等区域

  2. 栈内存是线程私有的,每个线程都有自己的栈空间

  3. 方法区(元空间)存储类信息、常量池等

  4. 常见的 OOM 类型:HeapSpace、OutOfMemoryError、StackOverflowError

总结

理解 JVM 内存模型是成为高级 Java 工程师的必备技能。在实际项目中,配置合适的堆内存大小、选择合适的垃圾收集器,都需要对内存布局有清晰的认识。


   转载规则


《JVM字节码与执行引擎》 小乐 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录