欢迎光临
我们一直在努力

(JVM_01)深入理解JVM与JMM:从跨平台到并发一致性的设计哲学

背景

Java 在诞生之初就喊出了“Write Once, Run Anywhere”的宏伟口号,这一愿景的实现全靠 Java 虚拟机(JVM)。它如同一台抽象的计算机,在字节码和真实硬件/操作系统之间架起桥梁,让程序员无需关心底层差异。然而随着多核CPU的普及,并发编程的噩梦也随之而来:同样的多线程代码,在 x86 服务器上平安无事,换到 ARM 架构的手机上却可能莫名其妙地崩溃。于是,Java 内存模型(JMM) 应运而生,为混乱的并发世界制定了一套统一的行为规范。

目的

  • JVM 的目标:屏蔽不同操作系统和 CPU 指令集的差异,确保同一个 .class 字节码在 Windows、Linux、macOS 上产生完全相同的运行结果。
  • JMM 的目标:屏蔽不同硬件内存架构(缓存、乱序执行等)带来的不确定性,确保多线程程序的并发逻辑在任何平台上都有可预测的、正确的行为。

两者协同工作,让 Java 真正做到“一次编写,到处正确运行”。

目录

  • JVM 整体架构
  • 运行时数据区:堆、栈与方法区
  • 类加载与双亲委派模型
  • 包名校验的双重防线
  • JMM 的设计初衷
  • happens-before 规则与实战案例
  • 总结

  • 一、JVM 整体架构

    可以把 JVM 看作一个精密运转的工厂,它主要由三个子系统构成:

    ┌─────────────────────────────────────────────┐
    │ JVM 架构 │
    ├───────────────┬─────────────┬───────────────┤
    │ 类加载器 │ 运行时数据区 │ 执行引擎 │
    │ (ClassLoader)│ (Runtime │ (Execution │
    │ │ Data Area) │ Engine) │
    └───────────────┴─────────────┴───────────────┘

    子系统职责
    类加载器 读取 .class 文件,将其转化为 JVM 内部的类元数据。
    运行时数据区 存放程序运行过程中的所有数据,包括对象、方法调用链等。
    执行引擎 负责解释执行字节码,并利用即时编译器(JIT)将热点代码编译为本地机器码。

    二、运行时数据区:堆、栈与方法区

    运行时数据区是 JVM 最核心的部分,也是 GC 调优和 OOM 排查的主战场。它分为线程共享区和线程隔离区两大类:

    运行时数据区
    ├─ 线程共享
    │ ├─ 堆 (Heap) ← 存放对象实例、数组,GC 发生在这里
    │ └─ 方法区 (Method Area)← 存放类信息、常量、静态变量(JDK 8 后为元空间)
    └─ 线程私有
    ├─ 虚拟机栈 (VM Stack) ← 方法调用对应的栈帧,存局部变量等
    ├─ 本地方法栈 (Native) ← 为 native 方法服务
    └─ 程序计数器 (PC Register)← 记录线程当前执行指令的地址

    问:堆、栈、年轻代、老年代到底什么关系?

    答:堆是存放对象的整块内存区域,而年轻代和老年代是堆内部按分代假说划分的两个子区域。栈是线程私有的另一块区域,完全独立于堆。

    • 对象在堆中的流转路径:Eden → Survivor → 老年代。
    • 栈中的变量(局部变量表)持有对象的引用,指向堆中的实例。
    • 栈用完自动释放,堆中的对象由 GC 负责回收。

    三、类加载与双亲委派模型

    问:什么是双亲委派模型?

    答:当一个类加载器收到加载请求时,它首先不会自己尝试加载,而是把请求向上委托给父加载器,只有在父加载器无法完成时,自己才会加载。 三层核心加载器:

    加载器负责范围
    Bootstrap ClassLoader JAVA_HOME/lib/rt.jar 等核心类库
    Extension ClassLoader JAVA_HOME/lib/ext/ 目录下的扩展库
    Application ClassLoader 用户类路径(classpath)下的类

    委托顺序:Application → Extension → Bootstrap。

    问:为什么要这样设计?

    答:主要有两个目的:

  • 防止核心类被篡改:例如 java.lang.String 必须由 Bootstrap 加载,保证全局唯一且安全。
  • 保证类的唯一性:一个类的唯一标识 = 全限定名 + 加载它的类加载器。不同加载器加载的同名类会被视为不同的类。

  • 四、包名校验的双重防线

    问:我可以自定义类加载器去加载 java.lang.String 吗?

    答:不可以。 JVM 设置了两道硬防线来阻止这种行为。

    • 第一道防线:双亲委派机制。在 loadClass() 方法中,加载请求层层向上委派,Bootstrap ClassLoader 在顶层就会捕获 java.lang.String 的加载请求并直接处理,自定义加载器根本没有机会执行 findClass()。
    • 第二道防线:defineClass() 中的包名校验。即使你打破委派(例如重写 loadClass 直接调用 findClass),JVM 也会在 defineClass() 的 preDefineClass() 中检查类名,若发现以 java. 开头,直接抛出 SecurityException。 更深一层,JVM 的 C++ 源码中还维护了一个硬编码的禁止包名列表(例如 java/, javax/, sun/ 等),任何类定义最终都会经过此检查,完全无法绕过。

    五、JMM 的设计初衷

    JMM 要解决的核心矛盾是:现代 CPU 的多级缓存和指令重排序虽然提升了性能,却让多线程程序的行为变得不可预测。JMM 的使命就是在这片混乱之上建立秩序。

    问:JMM 到底解决了什么问题?

    答:主要解决并发编程的三大痛点:

    痛点描述
    可见性 一个线程修改了共享变量,另一个线程能否立即看到?
    有序性 代码的执行顺序是否和书写顺序一致?(指令重排序会打破)
    原子性 诸如 i++ 这样的复合操作是否会被线程切换打断?

    JMM 通过定义一套 happens-before 规则,告诉 JVM 和编译器:在哪些关键位置必须禁止重排序、刷新缓存,从而保证上述三点的正确性。

    问:JVM 和 JMM 是什么关系?

    答:它们是相互协作的关系,都服务于“屏蔽底层差异”的统一哲学。

    • JVM 负责在纵向屏蔽不同操作系统的差异,让字节码到处运行。
    • JMM 负责在横向屏蔽不同 CPU 内存架构的差异,让多线程代码到处正确。 打个比方:JVM 是“联合国同声传译”,让各国代表都能听懂同一句话;JMM 则是“交通规则”,保证各国车辆在同一条路上不会撞车。

    六、happens-before 规则与实战案例

    问:什么是 happens-before 规则?

    答:如果操作 A happens-before 操作 B,那么 A 的结果对 B 可见,且执行顺序上 A 看起来在 B 之前。它是判断线程安全的最高准则。

    核心规则速览

    规则说明
    程序次序规则 一个线程内,前面的代码 happens-before 后面的。
    volatile 规则 对 volatile 变量的写 happens-before 后续对该变量的读。
    锁规则 对锁的解锁 happens-before 后续对同一个锁的加锁。
    传递性 A happens-before B,B happens-before C ⇒ A happens-before C。

    案例1:被重排序破坏的“双重检查锁定”单例

    public class Singleton {
    private static Singleton instance;

    public static Singleton getInstance() {
    if (instance == null) {
    synchronized (Singleton.class) {
    if (instance == null) {
    instance = new Singleton(); // 危险!
    }
    }
    }
    return instance;
    }
    }

    instance = new Singleton() 内部可分解为:

  • 分配内存
  • 初始化对象(执行构造方法)
  • 将引用指向内存地址
  • 步骤2和3可能被重排序,导致另一个线程拿到一个尚未初始化完成的对象,造成严重错误。 修复:将 instance 声明为 private static volatile Singleton instance,利用 volatile 的 happens-before 规则禁止重排序。

    案例2:永远停不下来的线程

    public class VisibilityTest {
    private static boolean stop = false;

    public static void main(String[] args) throws InterruptedException {
    new Thread(() -> {
    while (!stop) { /* 忙等 */ }
    System.out.println("线程结束");
    }).start();

    Thread.sleep(1000);
    stop = true; // 主线程修改,子线程可能永远看不到
    }
    }

    由于缺乏 happens-before 关系,子线程可能一直读取到自己工作内存中缓存的旧值 false,导致程序永不退出。 修复:将 stop 声明为 volatile。


    总结

    • JVM 架构以运行时数据区为核心,堆栈协作支撑对象生命周期,类加载器与双亲委派保障安全。
    • JMM 通过一系列 happens-before 规则,在性能和正确性之间取得平衡,让多线程代码在不同硬件上表现一致。
    • 两者共同构成了 Java “一次编写,到处正确运行” 的基石,理解它们是深入 Java 世界的必经之路。

    本文由AI整理生成

    赞(0)
    未经允许不得转载:171主机测评 » (JVM_01)深入理解JVM与JMM:从跨平台到并发一致性的设计哲学
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址