JVM 架构与 Java 内存模型(JVM Architecture & Memory Model)——真香也要懂原理,不然改个
开篇语
哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛
今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。
我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!
1. JVM 主要组件与职责(高层速览)
JVM(以 HotSpot 实现为准)可以大致拆成几个“子系统”:
-
类加载器子系统(ClassLoader subsystem)
- 负责把
.class字节码读入并构建Class对象。常见层次:Bootstrap(引导类加载器)、Extension(扩展类加载器)、Application(应用类加载器)。 - 加载过程包含验证、准备(为静态变量分配内存并设置默认值)、解析、初始化(执行
<clinit>)。
- 负责把
-
运行时数据区(Runtime Data Areas)
- 包括堆(Heap)、方法区(Method Area / Metaspace)、Java 栈(栈帧)、本地方法栈、程序计数器(PC)。(第 2 节详细讲)
-
字节码执行引擎(Execution Engine)
- 解释器(Interpreter):逐条解释执行 bytecode。
- 即时编译器(JIT):热点方法会被编译为本地机器码(C1/C2 或 Graal),以提高性能。
- 运行时优化:内联、逃逸分析、锁消除、锁粗化等。
-
垃圾收集器(Garbage Collector, GC)
- 管理堆内存并回收无用对象。常见收集器:Serial、Parallel、CMS(旧)、G1、ZGC、Shenandoah 等(HotSpot 提供多种实现用于不同场景)。
- GC 策略(分代、标记-清除、标记-压缩)影响应用停顿与吞吐。
-
本地接口(JNI)与本地方法栈
- JVM 可以调用本地(C/C++)库或系统调用,相关状态保存在本地方法栈中。
2. 堆 / 栈 /方法区 /本地方法栈 等内存分配(说人话的地图)
Java 程序在运行时主要会使用这些内存区域(关注点:线程隔离 vs 共享):
-
堆(Heap) —— 所有线程共享
- 存对象实例与数组。GC 的主要战场。
- 细分为 Young(Eden + Survivor)与 Old(Tenured)代。
- Java 8 及之后,类元数据从 PermGen 移到 Metaspace(本质是 native 内存),而方法区的概念仍在语义上存在(类元信息、常量池等)。
-
方法区 / Metaspace —— 所有线程共享
- 存放类元信息(类结构、常量池、静态变量等)。HotSpot 用 Metaspace(native 内存)实现,避免固定大小的 PermGen 问题。
-
Java 栈(Stack) —— 每个线程独有
- 每个方法调用会生成一个栈帧(局部变量表、操作数栈、动态链接、返回地址)。局部变量(基本类型和对象引用)存这里。注意:对象本身在堆上。
-
本地方法栈(Native method stack) —— 每个线程独有(供 JNI 等使用)
-
程序计数器(PC) —— 每个线程独有(记录当前字节码指令地址)
内存分配的实务提醒:
- 对象分配通常在堆,JVM 会优化:逃逸分析可将对象分配到栈或做标量替换。
- 大对象直接进入 Old(可能触发 Full GC),注意配置如
-XX:MaxTenuringThreshold等。 - Metaspace 过大/过小会导致类加载或本地内存问题(调整
-XX:MaxMetaspaceSize)。
3. Java 内存模型(JMM)与 happens-before 关系(核心规则)
JMM 是 Java 语言规范中关于多线程内存交互的抽象规范。它定义了线程之间如何通过内存读写通信,以及哪种执行结果是允许的。
核心术语:happens-before
如果操作 A happens-before 操作 B,则 A 的结果对 B 是可见的,且 A 的操作执行顺序不会被重排序到 B 之后。happens-before 关系是传递的。
一些常见的 happens-before 规则(非常重要)
-
程序次序规则(Program Order Rule):在单个线程内,按程序顺序写的操作
happens-before后面的操作。 -
Monitor(锁)规则:对同一 monitor,unlock(释放锁) happens-before subsequent lock(获取同一锁)。这意味着锁释放前对共享变量的修改对随后的锁获取线程可见。
-
volatile 规则:对一个
volatile变量的写happens-before后续对该变量的读。 -
线程启动/终止:
Thread.start():在调用线程中start()之前的操作happens-before新线程中的动作(new thread 的开始可见之前操作)。Thread.join():被 join 线程中的所有操作happens-before返回 join 的线程继续执行。
-
传递性:如果 A hb B 且 B hb C,那么 A hb C。
-
Final 字段:构造器结束且
this没被逸出时,其他线程读取 final 字段具有特殊的可见性保证。
结论:只要能证明写操作
happens-before读操作,那么就不会出现“不可见”的情况;否则 Java 允许各种重排序与缓存导致的不可见行为。
4. 可见性、重排序与 volatile 语义(细节与误区)
可见性(Visibility)
- 可见性问题是指:线程 A 修改了共享变量 x,但线程 B 迟迟看不到这个修改(仍读取到旧值)。产生原因:CPU 缓存、寄存器、编译器/JIT 优化(重排序)等。
synchronized(监视器)通过锁的释放/获取建立 hb 关系,保证可见性与互斥性(atomicity)。volatile仅保证可见性与禁止部分重排序,但不保证复合操作的原子性(例如count++不是原子)。
指令重排(Reordering)
- 编译器/JIT 与 CPU 都可能出于性能考虑重排序指令,前提是保持单线程语义不变。JMM 允许在不被
happens-before规则破坏的情况下做重排。 - 因此在多线程场景,如果没有恰当的 hb 保证,重排序会导致奇怪的结果(比如看到一半构造完成的对象引用)。
volatile 的语义(Java 5+)
-
对
volatile写会刷新到主内存(“写入-内存屏障”),对volatile读会从主内存读取(“读-内存屏障”)。在 HotSpot 实现里,volatile写会生成 StoreStore + StoreLoad 屏障,读会生成 LoadLoad + LoadStore 屏障(实现细节可以查 GC/HotSpot 文档)。 -
volatile的效果:- 可见性:写到 volatile 的值对随后读该 volatile 的线程可见。
- 有序性:禁止某些重排序,特别是
volatile写之前的操作不会被重排到volatile写之后,volatile读之后的操作不会被重排到volatile读之前(因此可以用于轻量级的“信号”与一些有序要求)。
-
不是锁:
volatile不保证互斥,也不保证复合操作(read-modify-write)的原子性。要保证原子性用synchronized、Atomic*或LongAdder等工具。
5. 实战练习:示例演示 volatile、synchronized 对可见性/重排序的影响
下面给出几个 Java 示例(可复制运行),帮助你直观理解。
示例 A:可见性(没有 volatile,可能看不到更新)
public class VisibilityDemo {
private static boolean running = true; // 非 volatile
public static void main(String[] args) throws Exception {
Thread t = new Thread(() -> {
System.out.println("Thread started");
while (running) {
// busy-wait
}
System.out.println("Thread finished loop");
});
t.start();
Thread.sleep(100); // 让线程 t 运行一会儿
System.out.println("Main will set running=false");
running = false; // 主线程修改
t.join();
System.out.println("Main exits");
}
}
说明:在某些 JVM/平台上,t 线程可能长期看不到 running=false(因为 running 被缓存在寄存器或 CPU cache 中),程序可能“死循环”。这就是可见性问题。将 running 改为 volatile 可保证 t 线程尽快看到修改。
private static volatile boolean running = true;
示例 B:volatile + 重排序(双重检查锁定 DCL,错误 vs 正确)
错误的 DCL(可能失败):
public class Singleton {
private static Singleton instance; // not volatile
private Singleton() {
// heavy init
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 1
}
}
}
return instance;
}
}
问题点:instance = new Singleton() 在底层会分成多个步骤(分配内存 -> 初始化 -> 赋值引用)。JIT/CPU 可能把“赋值引用”提前,使得其它线程看到 instance != null,但对象尚未初始化完成(构造器还没跑完),导致不可预期行为。
修复(正确写法):
public class Singleton {
private static volatile Singleton instance; // volatile 修复
private Singleton() { /* init */ }
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
volatile 保证了对 instance 的写与读有合适的内存屏障,避免重排序导致的“半初始化”可见。
示例 C:volatile 不保证原子性
public class VolatileAtomicity {
private static volatile int counter = 0;
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) counter++;
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) counter++;
});
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println("counter = " + counter); // 很可能小于 20000
}
}
说明:尽管 counter 是 volatile,但 counter++ 是读-改-写三步,不是原子操作。正确做法是使用 AtomicInteger 或 synchronized 来保证原子性。
6. 常见陷阱(千万别把 volatile 当成万能钥匙)
- 把
volatile当锁用:volatile只提供可见性和一定的有序性,不能保证复合操作原子性。 - 误解内存屏障:
volatile并非在所有方向上都完全禁止重排序,只在特定的读/写界点插入内存屏障(具体屏障类型与实现有关)。 - 认为
synchronized很慢:现代 JVM(HotSpot)有大量优化(biased locks、lock coarsening、lock elision),synchronized在很多场景已经非常高效且简单可靠。 - 忽略 final 字段语义:
final字段的特殊发布规则能帮助安全发布不可变对象,错误改写或this在构造中逸出会破坏这个保证。 - 在设计上滥用共享可变状态:即便你懂得
volatile/锁,也应尽量减少共享可变状态,优先考虑不可变对象或无锁并发容器(如ConcurrentHashMap)与原子类。
7. 排错与检查清单(遇到并发/可见性 bug 的实战步骤)
- 复现最小可复现例子:把问题缩小到最小程序,能复现就是胜利。
- 确定相关变量的访问点:找出哪些线程读写哪些共享变量,是否有同步手段(锁/volatile/atomic)保护。
- 检查发生顺序与 hb 关系:思考写操作是否
happens-before读操作,若无,则可能被允许不可见或重排。 - 替换或添加同步策略:对可疑变量尝试添加
volatile、synchronized、或使用Atomic*,看问题是否解决。 - 使用日志/断点/Thread dump:用日志标记关键点或用
jstack等捕获线程堆栈。 - 考虑工具与 JVM 参数:开启
-XX:+PrintAssembly(需要 hsdis)或使用 Java Flight Recorder、async-profiler、VisualVM 分析热点与锁情况。 - 代码审查与设计改进:是否能改为无共享或减小共享粒度?使用并发工具类(
BlockingQueue、ConcurrentHashMap、CompletableFuture)替代手工同步。
8. 延伸阅读(推荐权威资料)
- Java Language Specification(JLS)第 17 章:Java 内存模型 — 官方规范,最权威。
- The Java Memory Model(2004, Manson, Pugh 等)论文 — 解释 JMM 设计与 rationale。
- Brian Goetz 等《Java Concurrency in Practice》 — 经典并发指南(多数例子贴合 JMM)。
- HotSpot 源码与文档 — 想深入了解
volatile、内存屏障、JIT 优化与 GC 实现,读 HotSpot 实现很有帮助。 - 现代 GC 资料(G1、ZGC、Shenandoah) — 根据业务场景选择合适 GC。
9. 最后啰嗦几句(实战建议与心态)
- 并发问题三要素:可见性(visibility)、有序性(ordering)、原子性(atomicity)。用这三把钥匙去问问题:哪个被破坏了?
- 优先使用现成的并发工具(
Concurrent包、Atomic类、线程池),手写同步逻辑要非常小心。 volatile是轻量级信号(flag)或用于保证发布顺序(如 DCL 的instance),不是用来替代锁做复杂同步。- 当你写
synchronized的时候,先写出正确性,再优化性能(现代 JVM 会尽力把性能问题自动化处理)。 - 如果要在生产中保证高并发性能,理解 JMM 与 HotSpot 优化非常重要:逃逸分析、锁消除、内联等都可能影响你的并发假设。
… …
文末
好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。
… …
学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!
wished for you successed !!!
⭐️若喜欢我,就请关注我叭。
⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。
版权声明:本文由作者原创,转载请注明出处,谢谢支持!
- 点赞
- 收藏
- 关注作者
评论(0)