JVM技术之旅-带你吃透JMM知识体系

简介: JVM技术之旅-带你吃透JMM知识体系

前体概要


CPU和内存之间同样存在一致性问题。很多人认为CPU是一个计算组件,并没有数据一致性的问题但事实上,由于内存的发展速度跟不上CPU的更新,在CPU和内存之间,存在着多层的高速缓存。


原因就是由于多核所引起的,这些高速缓存,往往会有多层。如果一个线程的时间片跨越了多个CPU,那么同样存在同步的问题



import java.util.stream.IntStream;
public class JMMDemo {
    int value = 0;
    void add() {
        value++;
    }
    public static void main(String[] args) throws Exception {
        final int count = 100000;
        final JMMDemo demo = new JMMDemo();
        Thread t1 = new Thread(() -> IntStream.range(0, count).forEach((i) -> demo.add()));
        Thread t2 = new Thread(() -> IntStream.range(0, count).forEach((i) -> demo.add()));
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(demo.value);
复制代码


void add();
    descriptor: ()V
    flags:
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field value:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field value:I
        10: return
      LineNumberTable:
        line 7: 0
        line 8: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   LJMMDemo;
复制代码


另外,在执行过程中,CPU 可能也会对输入的代码进行乱序执行优化,Java 虚拟机的即时编译器也有类似的指令重排序优化,缓存内存机制的优化。整个函数的执行步骤就分的更加细致,看起来非常的碎片化(比字节码指令要细很多),不管是字节码的原因,还是硬件的原因,在粗粒度上简化来看,比较浅显且明显的因素,那就是线程 add 方法的操作并不是原子性的。


image.png


为了解决这个问题,我们可以在add方法上添加synchronized关键字,它不仅保证了内存上的同步,而且还保证了CPU的同步。这个时候,各个线程只能排队进入add方法


synchronized void add() {
    value++;
}


image.png

着重看一下 add 方法,可以看到一个简单的 i++ 操作,竟然有这么多的字节码,而它们都是傻乎乎按照“顺序执行”的。当它自己执行的时候不会有什么问题,但是如果放在多线程环境中,执行顺序就变得不可预料了。

image.png


由上图可以看出来CPU私有人Cache只有L1和L2,L3和L4(新版支持),都属于公用的。




问题分析


  • 1.(可见性问题+原子性问题)上图展示了这个乱序的过程。线程 A 和线程 B “并发”执行相同的代码块 add,执行的顺序如图中的标号,它们在线程中是有序的(1、2、5 或者 3、4、6),但整体顺序是不可预测的。


  • 2.(可见性问题)线程 A 和 B 各自执行了一次加 1 操作,但在这种场景中,线程 B 的 putfield 指令直接覆盖了线程 A 的值,最终 value 的结果是 101。


  • 3.(可见性问题)上面的示例仅仅是字节码层面上的,更加复杂的是,CPU 和内存之间同样存在一致性问题。很多人认为 CPU 是一个计算组件,并没有数据一致性的问题。但事实上,由于内存的发展速度跟不上CPU的更新,在CPU和内存之间,存在着多层的高速缓存,原因就是由于多核所引起的,这些高速缓存,往往会有多层。如果一个线程的时间片跨越了多个 CPU,那么同样存在同步的问题。


  • 4.(有序性问题)另外,在执行过程中,CPU 可能也会对输入的代码进行乱序执行优化,Java 虚拟机的即时编译器也有类似的指令重排序优化。整个函数的执行步骤就分的更加细致,看起来非常的碎片化(比字节码指令要细很多)。



并发场景


  • 单线程:CPU 核心的缓存只被一个线程访问。缓存独占,不会出现访问冲突等问题


  • 单核 CPU,多线程:进程中的多个线程会同时访问进程中的共享数据,CPU 将某块内存加载到缓存后,不同线程在访问相同的物理地址的时候,都会映射到相同的缓存位置,这样即使发生线程的切换,缓存仍然不会失效


  • 多核 CPU,多线程:每个核都至少有一个 L1 缓存 和 L2 缓存 。 多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的Cache中保留一份共享内存的缓冲,由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的 Cache 之间的数据就有可能不同


另一种情况(缺乏标准化和统一化)


  1. 在不同的硬件生产商和不同的操作系统下,内存的访问逻辑有一定的差异,结果就是当你的代码在某个系统环境下运行良好,并且线程安全,换了个系统就出现各种问题;


  1. 这是因为不同的处理器,在处理器优化和指令重排等方面存在差异,造成同样的代码,在经过不同的处理器优化和指令重排后,最后执行出来的结果可能不同,这是我们所不能接受的。


  1. JMM就应运而生了,究其根本就是为了解决在并发环境下,保证数据的安全,满足场景的可见性、原子性、有序性




基本概念


  • JMM是一个抽象的概念,它描述了一系列的规则或者规范,用来解决多线程的共享变量问题,比如 volatile、synchronized 等关键字就是围绕 JMM 的语法这里所说的变量,包括实例字段、静态字段,但不包括局部变量和方法参数,因为后者是线程私有的,不存在竞争问题。


  • JMM试图定义一种统一的内存模型,能将各种底层硬件,以及操作系统的内存访问差异进行封装,使Java程序在不同硬件及操作系统上都能达到相同的并发效果


JMM规定了共享变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存保存了主内存的副本拷贝,对变量的操作在工作内存中进行,不能直接操作主内存中的变量。不同线程间无法直接访问对方的工作内存变量,需要通过主内存完成。



JMM的结构


JMM分为主存储器(Main Memory)和工作存储器(Working Memory)两种


  • 主存储器是实例位置所在的区域,所有的实例都存在于主存储器内。比如,实例所拥有的字段即位于主存储器内,主存储器是所有的线程所共享的


  • 工作存储器是线程所拥有的作业区,每个线程都有其专用的工作存储器。工作存储器存有主存储器中必要部分的拷贝,称之为工作拷贝(Working Copy)


在这个模型中,线程无法对主存储器直接进行操作。如下图,线程A想要和线程B通信,只能通过主存进行交换。


image.png


  • 这些内存区域都是在哪存储的呢?如果非要有个对应的话,你可以认为主存中的内容是Java堆中的对象,而工作内存对应的是虚拟机栈中的内容


  • 但实际上,主内存也可能存在于高速缓存,或者CPU的寄存器上;工作内存也可能存在于硬件内存中,我们不用太纠结具体的存储位置



8个操作类型


支持JMM,Java定义8种原子操作,用来控制主存与工作内存之间的交互。


(1)read(读取)作用于主内存,它把变量从主内存传动到线程的工作内存中,供后面的 load 动作使用。


(2)load(载入)作用于工作内存,它把read操作的值放入到工作内存中的变量副本中。


(3)store(存储)作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用。


(4)write (写入)作用于主内存,它把store传送值放到主内存中的变量中。


(5)use(使用)作用于工作内存,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时,将会执行这个动作。


(6)assign(赋值)作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时,执行该操作。


(7)lock(锁定)作用于主内存,把变量标记为线程独占状态。


(8)unlock(解锁)作用于主内存,它将释放独占状态。










相关文章
|
2月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
77 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
6月前
|
监控 Java 调度
探秘Java虚拟机(JVM)性能调优:技术要点与实战策略
【6月更文挑战第30天】**探索JVM性能调优:**关注堆内存配置(Xms, Xmx, XX:NewRatio, XX:SurvivorRatio),选择适合的垃圾收集器(如Parallel, CMS, G1),利用jstat, jmap等工具诊断,解决Full GC问题,实战中结合MAT分析内存泄露。调优是平衡内存占用、延迟和吞吐量的艺术,借助VisualVM等工具提升系统在高负载下的稳定性与效率。
106 1
|
2月前
|
存储 SQL 小程序
JVM知识体系学习五:Java Runtime Data Area and JVM Instruction (java运行时数据区域和java指令(大约200多条,这里就将一些简单的指令和学习))
这篇文章详细介绍了Java虚拟机(JVM)的运行时数据区域和JVM指令集,包括程序计数器、虚拟机栈、本地方法栈、直接内存、方法区和堆,以及栈帧的组成部分和执行流程。
39 2
JVM知识体系学习五:Java Runtime Data Area and JVM Instruction (java运行时数据区域和java指令(大约200多条,这里就将一些简单的指令和学习))
|
2月前
|
Java 应用服务中间件 程序员
JVM知识体系学习八:OOM的案例(承接上篇博文,可以作为面试中的案例)
这篇文章通过多个案例深入探讨了Java虚拟机(JVM)中的内存溢出问题,涵盖了堆内存、方法区、直接内存和栈内存溢出的原因、诊断方法和解决方案,并讨论了不同JDK版本垃圾回收器的变化。
36 4
|
2月前
|
Arthas 监控 Java
JVM知识体系学习七:了解JVM常用命令行参数、GC日志详解、调优三大方面(JVM规划和预调优、优化JVM环境、JVM运行出现的各种问题)、Arthas
这篇文章全面介绍了JVM的命令行参数、GC日志分析以及性能调优的各个方面,包括监控工具使用和实际案例分析。
68 3
|
2月前
|
SQL 缓存 Java
JVM知识体系学习三:class文件初始化过程、硬件层数据一致性(硬件层)、缓存行、指令乱序执行问题、如何保证不乱序(volatile等)
这篇文章详细介绍了JVM中类文件的初始化过程、硬件层面的数据一致性问题、缓存行和伪共享、指令乱序执行问题,以及如何通过`volatile`关键字和`synchronized`关键字来保证数据的有序性和可见性。
35 3
|
2月前
|
缓存 前端开发 Java
JVM知识体系学习二:ClassLoader 类加载器、类加载器层次、类过载过程之双亲委派机制、类加载范围、自定义类加载器、编译器、懒加载模式、打破双亲委派机制
这篇文章详细介绍了JVM中ClassLoader的工作原理,包括类加载器的层次结构、双亲委派机制、类加载过程、自定义类加载器的实现,以及如何打破双亲委派机制来实现热部署等功能。
65 3
|
2月前
|
安全 Java API
🌟探索Java宇宙:深入理解Java技术体系与JVM的奥秘
本文深入探讨了Java技术体系的全貌,从Java语言的概述到其优点,再到Java技术体系的构成,以及JVM的角色。旨在帮助Java开发者全面了解Java生态,提升对Java技术的认知,从而在编程实践中更好地发挥Java的优势。关键词:Java, JVM, 技术体系, 编程语言, 跨平台, 内存管理。
43 2
|
2月前
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
59 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
|
2月前
|
小程序 Oracle Java
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
这篇文章是关于JVM基础知识的介绍,包括JVM的跨平台和跨语言特性、Class文件格式的详细解析,以及如何使用javap和jclasslib工具来分析Class文件。
58 0
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用