运行时数据区:Java 进程的内存地图
你有没有想过,当你执行 java -jar app.jar 的时候,JVM 是怎么规划和使用内存的?
就像一座城市有不同的区域——商业区、住宅区、工业区——JVM 的运行时数据区也被划分为不同的内存区域,每个区域有自己的职责和特点。
理解这些区域,是掌握 JVM 的第一步。
一、线程视角:私有还是共享?
JVM 的内存区域,从线程角度来看,分为两大类:
1.1 线程私有区域
这些区域每个线程独享,互不干扰:
- 程序计数器(Program Counter Register):当前线程执行的字节码行号
- 虚拟机栈(VM Stack):方法调用的栈帧
- 本地方法栈(Native Method Stack):Native 方法的调用栈
为什么需要线程私有?
因为每个线程都在同时执行不同的方法,如果这些区域是共享的,线程切换时保存和恢复上下文会变得极其复杂。想象一下,你正在写代码,突然有人把你写到一半的代码和别人的代码混在一起——那将是灾难。
1.2 线程共享区域
这些区域所有线程共享,生命周期与 JVM 进程一致:
- 堆(Heap):存储对象实例和数组
- 方法区(Method Area):存储类信息、常量、静态变量
1.3 直接内存(不属于 JVM 规范)
这是 HotSpot 特有的实现细节,使用本地内存:
- 直接内存(Direct Memory):通过
ByteBuffer.allocateDirect()分配 - 不属于堆内存,受
-XX:MaxDirectMemorySize限制 - 主要用于高性能 I/O 场景
二、整体架构图
┌─────────────────────────────────────────────────────────────────┐
│ JVM 进程内存 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 线程 1 │ │ 线程 2 │ ... 线程 N │
│ │ ┌───────────┐ │ │ ┌───────────┐ │ │
│ │ │ PC │ │ │ │ PC │ │ │
│ │ └───────────┘ │ │ └───────────┘ │ │
│ │ ┌───────────┐ │ │ ┌───────────┐ │ │
│ │ │ VM Stack │ │ │ │ VM Stack │ │ │
│ │ └───────────┘ │ │ └───────────┘ │ │
│ │ ┌───────────┐ │ │ ┌───────────┐ │ │
│ │ │ Native │ │ │ │ Native │ │ │
│ │ │ Stack │ │ │ │ Stack │ │ │
│ │ └───────────┘ │ │ └───────────┘ │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 线程共享区域 │ │
│ │ ┌───────────────┐ ┌───────────────────┐ │ │
│ │ │ 堆 │ │ 方法区 │ │ │
│ │ │ (Heap) │ │ (Method Area) │ │ │
│ │ │ │ │ - 类信息 │ │ │
│ │ │ 对象实例 │ │ - 常量池 │ │ │
│ │ │ 数组 │ │ - 静态变量 │ │ │
│ │ │ │ │ - JIT编译代码 │ │ │
│ │ └───────────────┘ └───────────────────┘ │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 直接内存 (Direct Memory) │ │
│ │ - ByteBuffer.allocateDirect() │ │
│ │ - NIO 零拷贝优化 │ │
│ └─────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘三、各区域的 OOM 风险
每个区域都有限制,超出限制就会报错:
| 区域 | 异常类型 | 触发场景 |
|---|---|---|
| 堆 | OutOfMemoryError: Java heap space | 对象创建过多,内存泄漏 |
| 虚拟机栈 | StackOverflowError | 递归调用过深 |
| 虚拟机栈 | OutOfMemoryError: unable to create new native thread | 线程创建过多 |
| 方法区 | OutOfMemoryError: Metaspace | 类加载过多(JDK 8+) |
| 方法区 | OutOfMemoryError: PermGen space | 类加载过多(JDK 7-) |
| 直接内存 | OutOfMemoryError: Direct buffer memory | NIO 使用不当 |
面试追问方向:
- 线程私有区域的 GC 行为是什么?(不 GC,随线程消亡而释放)
- 为什么堆内存溢出和栈内存溢出的表现不同?(堆是 OOM,栈是 StackOverflowError)
- 直接内存属于 JVM 内存吗?它和堆内存有什么区别?
四、一道经典面试题
问:JVM 内存区域中,哪些是线程私有的,哪些是线程共享的?
答:
- 线程私有:程序计数器、虚拟机栈、本地方法栈
- 线程共享:堆、方法区(JDK 8+ 是元空间)
- 直接内存:不算在 JVM 规范内,属于本地内存
追问:如果让你设计一个 Java 运行时,你会怎么划分内存区域?为什么这样划分?
这个问题考察的是你对内存管理的理解。线程私有区域需要保证线程安全隔离,线程共享区域需要考虑并发访问控制。
五、常见配置参数
bash
# 堆内存配置
-Xms256m # 初始堆大小
-Xmx512m # 最大堆大小
-Xmn128m # 新生代大小
-XX:NewRatio=2 # 老年代:新生代 = 2:1
# 栈配置
-Xss1m # 线程栈大小(JDK 11+ Linux x64 默认 1MB)
# 方法区配置(JDK 7)
-XX:PermSize=128m
-XX:MaxPermSize=256m
# 元空间配置(JDK 8+)
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=256m
# 直接内存配置
-XX:MaxDirectMemorySize=256m留给你的问题
了解了 JVM 的内存区域划分后,不知道你有没有想过:
既然堆内存是所有线程共享的,那多线程并发分配对象时,如何保证不出现冲突?
TLAB(Thread Local Allocation Buffer)就是为此而生的。这个设计非常巧妙——每个线程在 Eden 区有自己专属的小块内存,分配对象时先在本地缓冲区操作,只有缓冲区满了才需要加锁同步。
下一篇文章,我们就来详细聊聊堆内存的结构。
