运行时数据区回答“执行数据放在哪里”,Java 内存模型回答“多线程读写何时可见、允许怎样重排”。两者名称相似, 但解决的问题不同。把它们混在一起,会得出“对象在堆上所以线程安全”之类的错误结论。
Table of contents
Open Table of contents
JVM 规范中的运行时数据区
| 数据区 | 是否线程共享 | 保存内容 | 常见失败 |
|---|---|---|---|
| JVM 栈 | 否 | 方法栈帧、局部变量、操作数栈 | StackOverflowError |
| PC 寄存器 | 否 | 当前线程执行位置 | 通常不可直接诊断 |
| 本地方法栈 | 否 | Native 方法执行状态 | 实现相关 |
| 堆 | 是 | 对象和数组 | OutOfMemoryError: Java heap space |
| 方法区 | 是 | 类结构、运行时常量池、方法代码 | 实现相关 |
方法区是规范概念。HotSpot 自 Java 8 起主要使用本地内存中的**元空间(Metaspace)**保存类元数据,但“方法区等于 元空间”不是所有 JVM 的规范要求。
运行 jcmd <pid> VM.flags 和 jcmd <pid> GC.heap_info 可以观察 HotSpot 的实际配置。不要把线程转储里的栈帧数量
直接换算为堆占用;它们来自不同区域。
JMM 规定线程间关系
**JMM(Java Memory Model,Java 内存模型)**定义共享变量的可见性、原子性和有序性。关键规则是 happens-before(先行发生):若操作 A happens-before B,B 必须看到 A 的效果。
常见关系包括:
- 解锁 happens-before 后续对同一锁的加锁。
- 对
volatile变量的写 happens-before 后续对它的读。 - 启动线程前的操作 happens-before 新线程中的操作。
- 线程中的操作 happens-before 另一个线程成功
join()返回。
volatile 能提供可见性和一定的有序性,但 count++ 仍包含读取、计算和写入三个步骤,不会因此变成原子操作。
final class 停止信号 {
private volatile boolean stopped;
void stop() { stopped = true; }
void runLoop() {
while (!stopped) {
Thread.onSpinWait();
}
}
}
这个例子只适合演示可见性。生产代码通常使用中断、锁、队列或并发工具,不应让线程持续空转。
引用在栈上不等于对象私有
局部变量中的引用位于当前栈帧,但它指向的对象可以被其他线程共享。反过来,JIT 也可能通过逃逸分析消除看似在堆上的 对象。线程安全取决于共享方式和同步关系,不取决于一句“对象在堆还是栈”。
诊断内存问题时使用 Heap Dump、NMT 和 GC 日志;诊断并发可见性时检查锁、volatile、原子类和 happens-before。
证据工具也应对应问题类型。
下一步
阅读对象创建、布局、逃逸分析与分配路径,观察一个 new 在
HotSpot 中可能经历什么。