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

举报
喵手 发表于 2026/01/15 16:56:25 2026/01/15
【摘要】 开篇语哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,...

开篇语

哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区: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,可能看不到更新)

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

说明:尽管 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 !!!


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

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


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

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。