JVM 架构与 Java 内存模型(JVM Architecture & Memory Model)——真香也要懂原理,不然改个并发就炸了!

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

全文目录:

开篇语

哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区: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)的原子性。要保证原子性用 synchronizedAtomic*LongAdder 等工具。

5. 实战练习:示例演示 volatilesynchronized 对可见性/重排序的影响

下面给出几个 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}}

说明:尽管 countervolatile,但 counter++ 是读-改-写三步,不是原子操作。正确做法是使用 AtomicIntegersynchronized 来保证原子性。

6. 常见陷阱(千万别把 volatile 当成万能钥匙)

  1. volatile 当锁用volatile 只提供可见性和一定的有序性,不能保证复合操作原子性。
  2. 误解内存屏障volatile 并非在所有方向上都完全禁止重排序,只在特定的读/写界点插入内存屏障(具体屏障类型与实现有关)。
  3. 认为 synchronized 很慢:现代 JVM(HotSpot)有大量优化(biased locks、lock coarsening、lock elision),synchronized 在很多场景已经非常高效且简单可靠。
  4. 忽略 final 字段语义final 字段的特殊发布规则能帮助安全发布不可变对象,错误改写或 this 在构造中逸出会破坏这个保证。
  5. 在设计上滥用共享可变状态:即便你懂得 volatile/锁,也应尽量减少共享可变状态,优先考虑不可变对象或无锁并发容器(如 ConcurrentHashMap)与原子类。

7. 排错与检查清单(遇到并发/可见性 bug 的实战步骤)

  1. 复现最小可复现例子:把问题缩小到最小程序,能复现就是胜利。
  2. 确定相关变量的访问点:找出哪些线程读写哪些共享变量,是否有同步手段(锁/volatile/atomic)保护。
  3. 检查发生顺序与 hb 关系:思考写操作是否 happens-before 读操作,若无,则可能被允许不可见或重排。
  4. 替换或添加同步策略:对可疑变量尝试添加 volatilesynchronized、或使用 Atomic*,看问题是否解决。
  5. 使用日志/断点/Thread dump:用日志标记关键点或用 jstack 等捕获线程堆栈。
  6. 考虑工具与 JVM 参数:开启 -XX:+PrintAssembly(需要 hsdis)或使用 Java Flight Recorder、async-profiler、VisualVM 分析热点与锁情况。
  7. 代码审查与设计改进:是否能改为无共享或减小共享粒度?使用并发工具类(BlockingQueueConcurrentHashMapCompletableFuture)替代手工同步。

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 !!!


⭐️若喜欢我,就请关注我叭。

⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。

版权声明:本文由作者原创,转载请注明出处,谢谢支持!

Read more

最全盘点,赶紧收藏:2025 年全网最全的 Java 技术栈内容梳理(持续更新中)

重阳,2026 年初了(当前 2 月),但 2025 年底到 2026 年初的 Java 技术栈其实变化不算剧烈——主流还是 Spring Boot 3.x + JDK 21/17 LTS + 云原生 的组合,只是虚拟线程、GraalVM 原生镜像、AI 集成、observability 等方向加速落地。 下面给你一份 2025-2026 仍然非常主流且实用的全网最全 Java 技术栈梳理(后端为主,全栈/架构为辅),按 阶段 + 掌握深度 分层,标注了 2025-2026 的真实趋势和“是否强烈推荐”。 0. 基础环境 &

By Ne0inhk
C++之多态

C++之多态

多态 * 什么是多态? * 多态的定义及实现 * 多态的构成条件 * 虚函数 * 虚函数的重写/覆盖 * 关键技术原理 * 最佳实践指南 * 虚函数重写 * 协变 * 析构函数的重写 * override和final关键字 * 纯虚函数和抽象类 * 多态的原理 * 多态是如何实现的 * 1. 虚函数表(vtable) * 虚函数表知识要点 * 2. 虚函数的声明 * 3. 多态的实现过程 * 动态绑定与静态绑定 什么是多态? 多态(Polymorphism)是面向对象编程的三大核心特性之一(封装、继承、多态),源于希腊语"多种形态"。在C++中,它允许我们使用统一的接口处理不同类型的对象,显著提高了代码的灵活性和可扩展性。 核心概念 1. 同一接口,多种形态 不同的对象可以通过相同的方法名调用,但实际执行的逻辑由对象自身的类决定。 2. 解耦调用与实现 调用者只需关注接口(方法名和参数)

By Ne0inhk