深入拆解Java虚拟机
郑雨迪
Oracle 高级研究员,计算机博士
立即订阅
28017 人已学习
课程目录
已完结 39 讲
0/4登录后,你可以任选4讲全文学习。
开篇词 (1讲)
开篇词 | 为什么我们要学习Java虚拟机?
免费
模块一:Java虚拟机基本原理 (12讲)
01 | Java代码是怎么运行的?
02 | Java的基本类型
03 | Java虚拟机是如何加载Java类的?
04 | JVM是如何执行方法调用的?(上)
05 | JVM是如何执行方法调用的?(下)
06 | JVM是如何处理异常的?
07 | JVM是如何实现反射的?
08 | JVM是怎么实现invokedynamic的?(上)
09 | JVM是怎么实现invokedynamic的?(下)
10 | Java对象的内存布局
11 | 垃圾回收(上)
12 | 垃圾回收(下)
模块二:高效编译 (12讲)
【工具篇】 常用工具介绍
13 | Java内存模型
14 | Java虚拟机是怎么实现synchronized的?
15 | Java语法糖与Java编译器
16 | 即时编译(上)
17 | 即时编译(下)
18 | 即时编译器的中间表达形式
19 | Java字节码(基础篇)
20 | 方法内联(上)
21 | 方法内联(下)
22 | HotSpot虚拟机的intrinsic
23 | 逃逸分析
模块三:代码优化 (10讲)
24 | 字段访问相关优化
25 | 循环优化
26 | 向量化
27 | 注解处理器
28 | 基准测试框架JMH(上)
29 | 基准测试框架JMH(下)
30 | Java虚拟机的监控及诊断工具(命令行篇)
31 | Java虚拟机的监控及诊断工具(GUI篇)
32 | JNI的运行机制
33 | Java Agent与字节码注入
模块四:黑科技 (3讲)
34 | Graal:用Java编译Java
35 | Truffle:语言实现框架
36 | SubstrateVM:AOT编译框架
尾声 (1讲)
尾声 | 道阻且长,努力加餐
深入拆解Java虚拟机
登录|注册

13 | Java内存模型

郑雨迪 2018-08-20
我们先来看一个反常识的例子。
int a=0, b=0;
public void method1() {
int r2 = a;
b = 1;
}
public void method2() {
int r1 = b;
a = 2;
}
这里我定义了两个共享变量 a 和 b,以及两个方法。第一个方法将局部变量 r2 赋值为 a,然后将共享变量 b 赋值为 1。第二个方法将局部变量 r1 赋值为 b,然后将共享变量 a 赋值为 2。请问(r1,r2)的可能值都有哪些?
在单线程环境下,我们可以先调用第一个方法,最终(r1,r2)为(1,0);也可以先调用第二个方法,最终为(0,2)。
在多线程环境下,假设这两个方法分别跑在两个不同的线程之上,如果 Java 虚拟机在执行了任一方法的第一条赋值语句之后便切换线程,那么最终结果将可能出现(0,0)的情况。
除上述三种情况之外,Java 语言规范第 17.4 小节 [1] 还介绍了一种看似不可能的情况(1,2)。
造成这一情况的原因有三个,分别为即时编译器的重排序,处理器的乱序执行,以及内存系统的重排序。由于后两种原因涉及具体的体系架构,我们暂且放到一边。下面我先来讲一下编译器优化的重排序是怎么一回事。
首先需要说明一点,即时编译器(和处理器)需要保证程序能够遵守 as-if-serial 属性。通俗地说,就是在单线程情况下,要给程序一个顺序执行的假象。即经过重排序的执行结果要与顺序执行的结果保持一致。
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《深入拆解Java虚拟机》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(45)

  • 大场镇车王 置顶
    老师 为什么volatile内存屏障不允许所有写操作之前的读操作被重排序到写操作之后?前面不是说volatile的写操作happens before对用一字段的读操作吗

    作者回复: volatile字段的happens-before关系指的是在两个不同线程中,volatile的写操作 happens-before 之后对同一字段的读操作。这里有个关键字之后,指的是时间上的先后。也就是我这边写,你之后再读就一定能读得到我刚刚写的值。普通字段则没有这个保证。

    屏障不允许重排序是针对即时编译器的。写后对同一字段的读,属于数据依赖,本来也不可以重排序的。

    2018-08-20
    1
    11
  • 黑崽 置顶
    请教个问题。刚才有说不会把volatile放到寄存器,但是应该会在栈里面对吧。直接读取主内存,读写的是栈数据,然后利用堆内存和栈上数据是利用写缓存刷新同步的?

    作者回复: 我可能没有在原文中讲清楚。这里指的是volatile变量不能被分配到寄存器中,但是计算还是加载到寄存器中来计算的。

    所谓的分配到寄存器中,你可以理解为编译器将内存中的值缓存在寄存器中,之后一直用访问寄存器来代表对这个内存的访问的。假设我们要遍历一个数组,数组的长度是内存中的值。由于我们每次循环都要比较一次,因此编译器决定把它放在寄存器中,免得每次比较都要读一次内存。对于会更改的内存值,编译器也可以先缓存至寄存器,最后更新回内存即可。

    Volatile会禁止上述优化。

    2018-08-22
    1
    8
  • 追梦 置顶
    老师,求讲解下jvm中代码如何实现的内存屏障

    作者回复: 即时编译器生成的代码里会使用CPU的内存屏障指令。HotSpot采用的lock前缀的指令,lock add DWORD PTR [rsp] 0。它也会刷缓存。

    至于在即时编译器里禁止重排序所使用的”内存屏障”,就是一个特殊的编译器中间表达形式节点。

    2018-08-20
    2
  • Jerry Chan 置顶
    博客在哪里啊?

    作者回复: 文末[4]所指向的链接。

    2018-08-21
  • qpm 置顶
    老师我提个问题。一个共享对象的变量是非volatile的,那么这个变量的写入会先写到寄存器上,再写回内存吗?那么jvm是不是无论如何都不保证啥时候变量的值会写回内存。假如另一个线程加锁访问这个变量,是不是jvm也不保证它能拿到最新数据。

    作者回复: 对的!如果即时编译器把那个变量放在寄存器里维护,那么另一个线程也没辙。

    2018-08-20
    2
  • Alex Rao 置顶
    老师,我在一些技术文章里看到说 volatile 的变量是存在工作内存,这个工作内存是一个什么概念?

    作者回复: 工作内存是JMM抽象出来的一个概念。你可以映射到实际的CPU缓存。

    2018-08-20
  • godtrue
    恩,这节听了好几遍,也读了几遍,最后还是感觉蒙蒙的。
    下面说下我的感受:
    1:一图胜千言,尤其对于描述复杂的东西,这个建议其他同学也有提议的,希望雨迪采纳一下

    2:感觉有些概念没有解释,比如:
    2-1:Java内存模型,这节不就是要讲Java内存类型是什么?有什么特点?为什么这么设计嘛?不过我没看到这些内容,当然,特点是讲了的 happens-before 机制
    2-2:内存屏蔽,这是什么意思?它怎么就能禁止重排序啦?还有有其引申出的各种屏蔽是怎么回事呢?也没完全明白


    3:前面讲 as-if-serial 大概明白什么意思了,多处理器为了提高效率会采用流水线的方式来执行程序指令,但是同时要保证程序执行逻辑的正确性,所以,实际执行程序的指令和代码层面上会有不同,这个是由编译器来保证的,虽然执行逻辑不同但是程序逻辑是没变的,单线程没问题,但是多线程就变得复杂一些了,光靠这种方式保证不了啦,所以引出了下文

    4:happens-before,这个概念和作用比较容易理解,线程内有这种关系,线程间更有,这个机制我认为就是为了多线程环境下为了保证程序逻辑正确性的一种方式,不过它的具体实现细节感觉没理解,不清楚他是怎么办到的

    5:volatile 这个关键字之前也学习过,作用是保证内容的修改对所有线程可见,原理是修改后同步更新所有的内容,这是因为内存和处理器直接还是存在距离的,比如:内存-一级缓存-二级缓存-各种寄存器-cpu,如果是内存-cpu,则不会有这种问题了,不过性能也就不行了
    2018-08-23
    1
    34
  •  素丶  
    可以配合程晓明大大的《深入理解Java内存模型》
    https://www.infoq.cn/article/java_memory_model
    2018-11-20
    14
  • 啃yi嘴泥
    老师你好,关于指令重排序有点不太理解,指令重排序的粒度是方法级别的,还是整个源文件级别的。文中说道,b 加了volatile后,能够保证 b=1 先于r1=b ,这个我能理解,但是如何保证不会因为指令重排导致 b=1 先于r2=a发生呢?文中虽然说了,同一个线程中,字节码顺序暗含了r2=a happen before b=1,但是文中也提到了,拥有happen-before关系的两对赋值操作之间没有数据依赖,处理器可以指令重排序。r2=a 和b=1之间没有数据依赖呀!不好意思,这块有点迷糊,老师能给详细解答下不?

    作者回复: 首先,b加了volatile之后,并不能保证b=1一定先于r1=b,而是保证r1=b始终能够看到b的最新值。比如说b=1;b=2,之后在另一个CPU上执行r1=b,那么r1会被赋值为2。如果先执行r1=b,然后在另外一个CPU上执行b=1和b=2,那么r1将看到b=1之前的值。

    在没有标记volatile的时候,同一线程中,r2=a和b=1存在happens before关系,但因为没有数据依赖可以重排列。一旦标记了volatile,即时编译器和CPU需要考虑到多线程happens-before关系,因此不能自由地重排序。

    2018-08-24
    9
  • Kenneth
    同求讲解该问题,谢谢老师!:
    啃yi嘴泥
    老师你好,关于指令重排序有点不太理解,指令重排序的粒度是方法级别的,还是整个源文件级别的。文中说道,b 加了volatile后,能够保证 b=1 先于r1=b ,这个我能理解,但是如何保证不会因为指令重排导致 b=1 先于r2=a发生呢?文中虽然说了,同一个线程中,字节码顺序暗含了r2=a happen before b=1,但是文中也提到了,拥有happen-before关系的两对赋值操作之间没有数据依赖,处理器可以指令重排序。r2=a 和b=1之间没有数据依赖呀!不好意思,这块有点迷糊,老师能给详细解答下不?
    2018-08-24

    作者回复: 嗯,我回到原问题上哈

    2018-08-30
    1
    2
  • believe me
    即时编译器才会重排序,解释执行是不是就没有这个问题了?
    2019-06-03
    1
  • 阿巍-豆夫
    关于Volatile, 我想问下,如果是单个cpu的系统上运行多线程的程序,是不是这个volative就没有效果了? 因为大家都使用同一个寄存器。

    作者回复: 理论上,因为都使用同一套缓存,所以不需要volatile。实际实现中,对编译器不能重排列的限制还是存在的,但具体的memory barrier指令的实现是no-op。

    2018-12-03
    1
  • 冬末未末
    happen-before 在这里不能理解成在什么之前发生,它和时间没有任何关系。个人感觉解释成“生效可见于” 更准确。
    2018-09-27
    1
  • Kyle
    我个人理解的“JAVA内存模型”应该是包括两部分的内容:
    一是运行时数据区,
    二是定义了一组内存访问规则。

    这里其实主要讲的是其中的第二部分内容。不知道是不是可以这样总结。

    作者回复: 谢谢总结!确实,本文重点讲的是内存可见性规则。

    JMM的工作内存,主内存这些概念都是抽象的,对应实际体系架构中的缓存和内存。本文切掉了抽象的那部分,直接用实际的体系架构来讲解。

    2018-08-25
    1
  • 熊猫酒仙
    老师,既然有写缓存,是不是也有读缓存呢?强制刷新写缓存,无效化相应的内存数据,那么这些内存数据的读缓存也就失效了,需要重新加载最新数据,是否可以这样理解?

    另外强制刷新写缓存是否也有粒度一说?就是我们加的锁也有粒度之分,那么所触发的强制刷新写缓存的区域可能不一样?
    2018-08-22
    1
  • _____He
    老师,能不能对 happens-before 这个解释一下,总感觉好像没理解透
    2019-12-07
  • 放个屁臭到了自己
    前面几个有讲到 volatile 可能会导致缓存行失效,但是如果使用了 volatile 的话,不是直接从内存中取嘛?

    2019-12-05
  • 放个屁臭到了自己

    int a=0, b=0;

    public void method1() {
      int r2 = a;
      b = 1;
    }

    public void method2() {
      int r1 = b;
      a = 2;
    }

    这里说的是即时编译器, JIT 也就是运行的时候吧,那这样的话,运行时就能知道了?是不是单线程调用的了?
    2019-12-05
  • 放个屁臭到了自己

    int a=0, b=0;

    public void method1() {
      int r2 = a;
      b = 1;
    }

    public void method2() {
      int r1 = b;
      a = 2;
    }


    编译器是怎么知道他是单线程会吊用,还是多线程会调用呢?
    2019-12-05
  • 长脖子树
    测试用例 [6] 中实际测试后会发现, 有一部分的 object 尚未初始化
    问题出在 new 操作上,我们以为的 new 操作应该是:
    分配一块内存 M;在内存 M 上初始化 Singleton 对象;然后 M 的地址赋值给 instance 变量。
     但是实际上优化后的执行路径却是这样的:
    分配一块内存 M;将 M 的地址赋值给 instance 变量;最后在内存 M 上初始化 Singleton 对象。
    所谓的单例模式, 就是安全发布的问题
    (部分来自专栏 java并发编程实战)
    2019-11-06
收起评论
45
返回
顶部