JVM 架构深度解析
JVM(Java Virtual Machine)是 Java "一次编写,到处运行"的基石。深入理解 JVM,是成为高级 Java 工程师的必经之路。
JVM 整体架构
┌─────────────────────────────────────────────────────┐
│ Java 源代码 (.java) │
└──────────────────────┬──────────────────────────────┘
│ javac 编译
┌──────────────────────▼──────────────────────────────┐
│ 字节码文件 (.class) │
└──────────────────────┬──────────────────────────────┘
│
┌──────────────────────▼──────────────────────────────┐
│ JVM │
│ ┌─────────────────────────────────────────────┐ │
│ │ 类加载子系统 │ │
│ │ Bootstrap → Extension → Application │ │
│ └──────────────────┬──────────────────────────┘ │
│ │ │
│ ┌──────────────────▼──────────────────────────┐ │
│ │ 运行时数据区 │ │
│ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────────┐ │ │
│ │ │方法区 │ │ 堆 │ │ 栈 │ │本地方法栈 │ │ │
│ │ │(元空间)│ │ │ │(线程)│ │ │ │ │
│ │ └──────┘ └──────┘ └──────┘ └──────────┘ │ │
│ │ 程序计数器(每线程独立) │ │
│ └──────────────────┬──────────────────────────┘ │
│ │ │
│ ┌──────────────────▼──────────────────────────┐ │
│ │ 执行引擎 │ │
│ │ 解释器 │ JIT 编译器 │ GC 垃圾回收器 │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 本地方法接口 (JNI) │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘运行时数据区详解
程序计数器(PC Register)
- 线程私有,每个线程独立一份
- 记录当前线程正在执行的字节码指令地址
- 执行 native 方法时值为
undefined - 唯一不会发生 OOM 的区域
Java 虚拟机栈(JVM Stack)
- 线程私有,生命周期与线程相同
- 每次方法调用创建一个栈帧(Stack Frame)
栈帧结构:
┌─────────────────────────┐
│ 局部变量表 │ 存储方法参数和局部变量
├─────────────────────────┤
│ 操作数栈 │ 执行字节码指令的工作区
├─────────────────────────┤
│ 动态链接 │ 指向运行时常量池的方法引用
├─────────────────────────┤
│ 方法返回地址 │ 方法正常/异常退出后的返回位置
└─────────────────────────┘java
// 演示栈帧
public class StackDemo {
public static void main(String[] args) {
// main 方法对应一个栈帧
int result = add(1, 2); // 调用 add,压入新栈帧
System.out.println(result);
}
public static int add(int a, int b) {
// add 方法的栈帧
// 局部变量表:a=1, b=2
return a + b; // 方法返回,栈帧弹出
}
}异常:
StackOverflowError:栈深度超过限制(递归过深)OutOfMemoryError:栈扩展时内存不足
bash
# 设置栈大小(默认 512KB-1MB)
java -Xss2m MyApp堆(Heap)
- 线程共享,JVM 最大的内存区域
- 所有对象实例和数组都在堆上分配
- GC 的主要管理区域
堆内存结构(以 G1 GC 为例):
┌─────────────────────────────────────────┐
│ 堆 │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ 年轻代 │ │ 老年代 │ │
│ │ ┌──┐ ┌──┐ │ │ │ │
│ │ │Eden│S0│S1 │ │ 长期存活对象 │ │
│ │ └──┘ └──┘ │ │ │ │
│ └──────────────┘ └──────────────────┘ │
└─────────────────────────────────────────┘bash
# 堆内存参数
-Xms512m # 初始堆大小
-Xmx2g # 最大堆大小
-Xmn512m # 年轻代大小
-XX:NewRatio=2 # 老年代:年轻代 = 2:1
-XX:SurvivorRatio=8 # Eden:S0:S1 = 8:1:1方法区 / 元空间(Metaspace)
- 线程共享
- 存储:类信息、常量、静态变量、JIT 编译后的代码
- Java 8 之前叫永久代(PermGen),Java 8+ 改为元空间(Metaspace)
- 元空间使用本地内存,不再受堆大小限制
bash
# 元空间参数(Java 8+)
-XX:MetaspaceSize=256m # 初始元空间大小
-XX:MaxMetaspaceSize=512m # 最大元空间大小java
// 运行时常量池(方法区的一部分)
String s1 = "hello"; // 字符串常量池
String s2 = "hello";
System.out.println(s1 == s2); // true,同一个常量池对象
String s3 = new String("hello"); // 堆上新对象
System.out.println(s1 == s3); // false
String s4 = s3.intern(); // 返回常量池中的引用
System.out.println(s1 == s4); // true对象的创建过程
new MyObject()
↓
1. 检查类是否已加载(类加载子系统)
↓
2. 分配内存
- 指针碰撞(内存规整时,Serial/ParNew GC)
- 空闲列表(内存不规整时,CMS GC)
- TLAB(Thread Local Allocation Buffer)线程本地分配缓冲
↓
3. 初始化零值(int=0, boolean=false, 引用=null)
↓
4. 设置对象头(Mark Word + 类型指针)
↓
5. 执行 <init> 方法(构造函数)对象内存布局
对象在堆中的布局:
┌─────────────────────────────┐
│ 对象头 │
│ ┌──────────────────────┐ │
│ │ Mark Word (8 bytes) │ │ 哈希码、GC年龄、锁状态
│ ├──────────────────────┤ │
│ │ 类型指针 (4/8 bytes) │ │ 指向类元数据
│ └──────────────────────┘ │
├─────────────────────────────┤
│ 实例数据 │ 字段值
├─────────────────────────────┤
│ 对齐填充 │ 保证 8 字节对齐
└─────────────────────────────┘java
// 使用 JOL 查看对象布局
// <dependency>
// <groupId>org.openjdk.jol</groupId>
// <artifactId>jol-core</artifactId>
// </dependency>
import org.openjdk.jol.info.ClassLayout;
public class ObjectLayout {
public static void main(String[] args) {
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
// java.lang.Object object internals:
// OFFSET SIZE TYPE DESCRIPTION
// 0 4 (object header: mark)
// 4 4 (object header: class)
// 8 0 (object alignment)
// Instance size: 8 bytes
}
}执行引擎
解释器 vs JIT 编译器
字节码执行方式:
解释执行:逐条解释字节码 → 启动快,执行慢
JIT 编译:热点代码编译为本地机器码 → 启动慢,执行快
混合模式(默认):
- 启动时解释执行
- 热点方法(调用次数超过阈值)触发 JIT 编译
- C1 编译器(Client):快速编译,优化少
- C2 编译器(Server):慢速编译,深度优化
- Graal 编译器(Java 10+):实验性,更激进的优化bash
# JIT 相关参数
-XX:CompileThreshold=10000 # 方法调用多少次触发 JIT(默认 10000)
-XX:+PrintCompilation # 打印 JIT 编译信息
-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining # 打印内联信息
-Xint # 纯解释模式(关闭 JIT)
-Xcomp # 纯编译模式常用 JVM 诊断命令
bash
# 查看 JVM 进程
jps -l
# 查看 JVM 参数
jinfo -flags <pid>
# 查看堆内存使用
jstat -gc <pid> 1000 # 每秒刷新
# 生成堆转储文件
jmap -dump:format=b,file=heap.hprof <pid>
# 查看线程状态
jstack <pid>
# 可视化工具
jconsole # JDK 自带
jvisualvm # JDK 自带(功能更强)
# 推荐:JProfiler、YourKit(商业)、Arthas(开源)JVM 版本演进
- Java 8:G1 GC 成为默认,Lambda/Stream,元空间
- Java 11:ZGC 引入,HTTP Client 标准化,LTS
- Java 17:Sealed Classes,Pattern Matching,LTS
- Java 21:Virtual Threads,Sequenced Collections,LTS(当前推荐)