HotSpot JVM 「03」Java Memory Model

01-什么是 JMM (Java Memory Model)?

《Java Language Specification》中对内存模型的描述如下:

A memory model describes, given a program and an execution trace of that program, whether the execution trace is a legal execution of the program.

简单翻译一下就是:对于任意一个程序和它的一次执行序列,内存模型描述了改执行序列是否是该程序的一次合法的执行。换句话说,内存模型描述了程序可能的行为。

在 Java 中,内存被大致分为两类:堆内存和栈内存。对内存由所有线程间共享,而栈内存则由各线程独享。下图很好的说明了堆内存和栈内存之间的关系。

Snipaste_2022-03-03_15-12-34.png

上图中,每个线程独享的内存包括两部分内容:pc 寄存器,存储了 JVM 要执行的下一条指令的地址;线程栈,保存了线程执行的方法调用情况。

在 Java 中,基本类型(例如整型、浮点型)的值是存储在线程栈空间的,而复合类型(如类类型)的值(即对象)是存储在堆空间中的,栈空间中仅保存指向该对象的引用。因此,线程间的数据交换根据数据类型的不同而有所区别:对基本类型,会完全拷贝一个副本存储在另一个线程的栈空间中;而对于对象,拷贝的则是对象的引用。对象本身仍然存储在堆空间中。

02-JMM 与硬件之间的关系

JMM 是为 Java 进程提供的一个逻辑上的抽象,也是 Java 程序实现可移植性的重要基础。但是,现代 CPU 硬件架构与 JMM 之间其实是有不同之处的。了解硬件架构,特别是 JMM 是如何与硬件架构互动的,对我们更进一步理解 Java 程序的运行有很大帮助。

了解计算机的读者基本上都知道一个事实,那就是 CPU 与内存之间在读写速度上存在着很大的差异,所以为了提高 CPU 利用率,在 CPU 与 RAM 内存之间存在一个多级缓存系统。这里我们不去考虑多级缓存之间的区别,而笼统地将其看作是 CPU 与内存之间的缓存。下图是现代计算机硬件架构的一个简化版示意图:

图1. 现代计算机硬件架构简化示意图

图1. 现代计算机硬件架构简化示意图 [jenkov.com]

扫描二维码关注公众号,回复: 14393330 查看本文章

图2. 堆上共享对象在多线程环境下的可见性问题

图2. 堆上共享对象在多线程环境下的可见性问题[jenkov.com]

现代计算机一般都拥有多个 CPU,每个 CPU 都能运行一个线程。所以,这就意味着 Java 中的多线程程序,其中的线程可能真的是在并行执行。在每个 CPU 中都存在着一组寄存器,这是计算机中访问速度最快的设备了。在寄存器外围,是访问速度较为逊色的高速缓存系统,再外围才是我们熟悉的 RAM 内存。设备的存储容量从内向外逐渐提升,访问速度逐渐降低。

JMM 中栈内存、对内存与实际的硬件设备之间的关系又是怎么样的呢?从上图中可以看出,在硬件架构上是不区分栈和堆的,栈和堆中的数据都存放在 RAM 主存中。但这样会存在一个问题。我们结合前一章的内容来分析一下。

  1. 对于线程栈中的内容,该部分内容是线程独享的,例如一些基本类型的值。此时,多级缓存系统是没有影响的。
  2. 对于堆上的对象,该部分内容可能在多个线程之间共享,线程通过栈中的引用来访问对象的内容。此时,多级缓存系统的存在可能会导致可见性问题。如上图2所示,堆上对象obj有一个count属性。当线程 a 访问该对象时,考虑 CPU 读写速度,有一部分内存(obj.count变量值所在的页)被加载到 CPU 缓存中,线程 a 对其做的修改会暂时驻留在缓存中,并不会立即写回到主存。这是因为决定缓存何时写回到内存并非取决于线程而是取决于操作系统。若此时线程 a 将obj.count修改为2。那么,线程 a 所在的 CPU 缓存中,obj.count=2,主存中obj.count=1。若此时线程 b 需要访问obj.count,则obj.count所在页会被加载到线程 b 所在的 CPU 的缓存中,且obj.count=1。这就是所谓的可见性问题,即此例中线程 a 对obj.count的修改对线程 b 不可见。如何解决这个问题?可以通过 volatile 关键字修饰变量obj.count。关键字 volatile 的语义是告诉操作系统从主存中读取变量并将对其的修改立即写回到主存。

03-Happens-before 关系

JSR-133 对 Java 中提出了新的内存模型,从 JDK 1.5 开始,Java 使用新的内存模型。JSR-133 中一个非常重要的内容就是提出了 happens-before 关系来描述操作之间的可见性。Happens-before 是一个关系,描述的是操作之间的关系。JLS 中对 Happens-before 的定义如下:

Two actions can be ordered by a happens-before relationship. If one action happens-before another, then the first is visible to and ordered before the second.

简单来说,具有 happens-before 关系的两个操作,前者操作的结果一定对后一个操作可见。在 JLS 中操作(action)的形式定义为四元组<t, k, v, u>,分别表示执行操作的线程、操作的类型、操作涉及的变量或监视器、操作的唯一标识符。

常见的 happens-before 关系有:

  • 单个线程中(顺序指令),前一操作 happens-before 任意后续的操作。
  • 对 monitor 的解锁操作 happens-before 后续对同一 monitor 的加锁操作。
  • 对 volatile 变量的写操作 happens-before 后续对该变量的读操作。

Happens-before 关系是满足传递性的,即操作A happens-before B,B happens-before C,则 A happens-before C。

Happens-before 与数据竞争

如果对同一个变量的两个操作(读或写)至少有一个是写操作,那么这两个操作被称为是冲突的(conflicting)。当程序中包含冲突操作,且冲突的两个操作并不具有 happens-before 关系,那么则称程序存在数据竞争(data race)。

多线程情形下,存在数据竞争的程序是非线程安全的,即程序的执行往往产生违反直觉的结果。

Happens-before 与重排序

JMM 是一个语言层面的内存模型,是一个抽象的概念。在设计 JMM 时需要同时考虑程序员对内存模型的使用,以及编译器、CPU 对内存模型的实现。前者要求内存模型易理解、易用,而后者要求内存模型约束尽可能地小,以便于实现以及各种性能优化(例如指令重排序)。这两种诉求往往是矛盾的,所以 JMM 在设计时需要权衡两种诉求。

在 JSR-133 中,JMM 把 happens-before 要求禁止的重排序分为了两类,并分类采取不同的措施:

  1. 会改变程序运行结果的重排序,JMM 要求编译器和 CPU 禁止这种重排序;
  2. 不会改变程序运行结果的重排序,JMM 允许这类重排序。

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿


参考资料

[1] The Java Language Specification Java SE 11 Edition

[2] JVM 基础 - Java 内存模型引入

[3] JVM 基础 - Java 内存模型详解

猜你喜欢

转载自juejin.im/post/7123577558431432734