JVM 整体架构与运行时数据区全景总览
JVM(Java Virtual Machine)是 Java 字节码的运行时环境。以 HotSpot 虚拟机为例,一次 Java 程序的执行可以拆成三个子系统:类加载子系统、运行时数据区和执行引擎。
HotSpot 架构总览
┌────────────────────────────────────────────────────────────┐
│ 类加载子系统(Class Loader) │
│ 加载 .class → 验证/准备/解析 → 初始化 → 交给运行时数据区 │
└──────────────────────────┬─────────────────────────────────┘
▼
┌────────────────────────────────────────────────────────────┐
│ 运行时数据区 │
│ 线程共享:堆(Heap)|方法区(元空间 Metaspace) │
│ 线程私有:虚拟机栈|本地方法栈|程序计数器 │
└──────────────────────────┬─────────────────────────────────┘
▼
┌────────────────────────────────────────────────────────────┐
│ 执行引擎 │
│ 解释器(字节码 → 逐条解释)|JIT 编译器(热点代码编译为机器码) │
│ 垃圾回收器(GC) │
└────────────────────────────────────────────────────────────┘三个子系统各司其职:类加载子系统把字节码读入内存,运行时数据区提供存储空间,执行引擎负责解释或编译执行。三者的协调配合构成了 JVM 的运行主链路。
运行时数据区六大区域
运行时数据区按"线程是否共享"可分为两类。线程共享区有堆和方法区,线程私有区有虚拟机栈、本地方法栈和程序计数器,此外还有一个不归运行时数据区管辖但常被讨论的直接内存。
| 区域 | 线程 | 存储内容 | 异常 |
|---|---|---|---|
| 堆 Heap | 共享 | 对象实例、数组 | OutOfMemoryError |
| 方法区(元空间) | 共享 | 类信息、常量池、静态变量 | OutOfMemoryError: Metaspace |
| 虚拟机栈 | 私有 | 栈帧(局部变量表、操作数栈等) | StackOverflowError / OOM |
| 本地方法栈 | 私有 | native 方法调用 | StackOverflowError |
| 程序计数器 | 私有 | 当前线程执行的字节码行号 | 无异常 |
| 直接内存 | 共享 | NIO 堆外缓冲 | OOM(受 MaxDirectMemorySize 限制) |
堆(Heap)
所有线程共享,是对象和数组的分配场所,也是 GC 的主战场。逻辑上分新生代(Eden、Survivor0、Survivor1)和老年代,通过 -Xms、-Xmx、-Xmn 控制大小。
方法区(元空间)
JDK 8 起以**元空间(Metaspace)**替代永久代,存储类元信息、运行时常量池、静态变量、JIT 编译产物等。元空间使用本地内存(直接内存),不受堆大小限制,默认只受操作系统内存约束。
虚拟机栈
每个线程私有,生命周期与线程一致。每个方法调用创建一个栈帧入栈,方法结束出栈。栈帧内包含局部变量表、操作数栈、动态链接、方法出口四部分。
本地方法栈
为虚拟机使用 native 方法服务。HotSpot 将本地方法栈与虚拟机栈合并实现,职责上仍然区分:native 方法调用由本地方法栈管理。
程序计数器
当前线程所执行字节码的行号指示器。分支、循环、跳转、异常恢复等依赖它定位下一条指令。唯一不会抛出 OOM 的区域。
直接内存
位于 JVM 堆外,由 NIO 的 DirectByteBuffer 或 Unsafe.allocateMemory 分配。优点是避免堆内对象与内核缓冲区之间的数据拷贝,常用于网络通信、文件读写的高吞吐场景。
常用参数速查
| 参数 | 作用 | 示例 |
|---|---|---|
-Xms | 初始堆大小 | -Xms512m |
-Xmx | 最大堆大小 | -Xmx2g |
-Xmn | 新生代大小 | -Xmn256m |
-XX:SurvivorRatio | Eden:S0:S1 比例 | -XX:SurvivorRatio=8 |
-XX:NewRatio | 老年代:新生代比例 | -XX:NewRatio=2 |
-XX:MetaspaceSize | 元空间初始大小 | -XX:MetaspaceSize=128m |
-XX:MaxMetaspaceSize | 元空间最大大小 | -XX:MaxMetaspaceSize=256m |
-XX:+UseCompressedOops | 开启对象指针压缩(64 位默认开) | -XX:+UseCompressedOops |
-XX:MaxDirectMemorySize | 直接内存上限(默认等于 -Xmx) | -XX:MaxDirectMemorySize=256m |
-Xss | 单线程栈大小 | -Xss512k |
常见面试点
- 一个 Java 对象从创建到回收经历了哪些区域? 新建对象优先分配在 Eden 或 TLAB;大对象直接进老年代;经过多次 Minor GC 存活后晋升老年代;最终由 GC 回收并可能被分配到连续内存(若有压缩整理)。
- 哪些区域会抛 StackOverflowError? 虚拟机栈和本地方法栈。栈深度超过
-Xss设定即抛;若栈允许动态扩展但无法申请到内存,则抛 OOM。 - 元空间和永久代有什么区别? 永久代是堆的一部分,受
-XX:MaxPermSize限制;元空间位于本地内存,默认上限仅受操作系统限制,降低了方法区 OOM 的概率。 - 程序计数器为什么不会 OOM? 它只保存一条字节码行号,大小固定且极小,不会因程序运行而增长,因此规范中不定义该区域的 OOM。