JVM 架构与 Java 内存模型(JVM Architecture & Memory Model)——真香也要懂原理,不然改个并发就炸了!
㊗️本期内容已收录至专栏《Java核心实操(进阶版)》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~
㊙️本期难度指数:⭐⭐⭐
🉐福利:一次订阅后,专栏内的所有文章可永久免费看,持续更新中,保底1000+(篇)硬核实战内容。

全文目录:
- 开篇语
- 1. JVM 主要组件与职责(高层速览)
- 2. 堆 / 栈 /方法区 /本地方法栈 等内存分配(说人话的地图)
- 3. Java 内存模型(JMM)与 happens-before 关系(核心规则)
- 4. 可见性、重排序与 `volatile` 语义(细节与误区)
- 5. 实战练习:示例演示 `volatile`、`synchronized` 对可见性/重排序的影响
- 6. 常见陷阱(千万别把 `volatile` 当成万能钥匙)
- 7. 排错与检查清单(遇到并发/可见性 bug 的实战步骤)
- 8. 延伸阅读(推荐权威资料)
- 9. 最后啰嗦几句(实战建议与心态)
- 文末
开篇语
哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区: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,可能看不到更新)
publicclassVisibilityDemo{privatestaticboolean running =true;// 非 volatilepublicstaticvoidmain(String[] args)throwsException{Thread t =newThread(()->{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 线程尽快看到修改。
privatestaticvolatileboolean running =true;示例 B:volatile + 重排序(双重检查锁定 DCL,错误 vs 正确)
错误的 DCL(可能失败):
publicclassSingleton{privatestaticSingleton instance;// not volatileprivateSingleton(){// heavy init}publicstaticSingletongetInstance(){if(instance ==null){synchronized(Singleton.class){if(instance ==null){ instance =newSingleton();// 1}}}return instance;}}问题点:instance = new Singleton() 在底层会分成多个步骤(分配内存 -> 初始化 -> 赋值引用)。JIT/CPU 可能把“赋值引用”提前,使得其它线程看到 instance != null,但对象尚未初始化完成(构造器还没跑完),导致不可预期行为。
修复(正确写法):
publicclassSingleton{privatestaticvolatileSingleton instance;// volatile 修复privateSingleton(){/* init */}publicstaticSingletongetInstance(){if(instance ==null){synchronized(Singleton.class){if(instance ==null){ instance =newSingleton();}}}return instance;}}volatile 保证了对 instance 的写与读有合适的内存屏障,避免重排序导致的“半初始化”可见。
示例 C:volatile 不保证原子性
publicclassVolatileAtomicity{privatestaticvolatileint counter =0;publicstaticvoidmain(String[] args)throwsException{Thread t1 =newThread(()->{for(int i =0; i <10000; i++) counter++;});Thread t2 =newThread(()->{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 !!!
⭐️若喜欢我,就请关注我叭。
⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。
版权声明:本文由作者原创,转载请注明出处,谢谢支持!