JVM-原来可以这样理解Java内存模型的工作过程和原理

前言

这段时间在阅读周志明的《深入理解Java虚拟机》,收获颇多;对Java内存模型之间的工作过程体会很深,这边做一个分享。这边分享的主要是针对java内存模型之间内存的工作做分享,不涉及过多的其他内容。

从内存的角度

从内存的角度来看Java的内存模型,我们大致可以把它分为主内存和工作内存;它们之间的关系很像计算机硬件层面的主内存和高速缓存的关系。

  • 主内存:由所有线程所共享的内存块
  • 工作内存:由线程持有,随线程而生,随线程而灭

Java内存模型中的Java堆和方法区,是所有线程所共享的,实际上对应Java内存就是主内存;
Java内存模型中的虚拟机栈,本地栈,计数器是线程独占的,实际上对应Java内存就是工作内存。
所以我们要分析Java的内存之间的工作过程首先要堆java的内存模型和这些概念有深刻的理解;他们之间的工作过程宏观上可以用下面的图来体现:
在这里插入图片描述

内存之间的交互的深度分析

上面的图宏观上展示了Java线程的工作内存和主内存的合作关系;接下来我们开始分析更加底层的原理。我们知道Java的内存中不同的区块有不同的功能特点,在分析之前我们先来回顾下,有助于下面的理解;

程序计数器(线程的私有内存)
  • 每条线程都有独立的计数器,每个线程的计数器互相不影响,独立存储(线程的私有内存)
  • 在任何一个确定的时刻,一个处理器(多核处理器即为一个内核)都会执行一条线程中的指令,程序计数器能使线程切换后恢复到正确的执行位置
  • 线程执行Java方法—计数器记录正在执行的的虚拟机字节码指令位置 线程执行Native方法----计数器为空
  • 程序计数器是虚拟机中唯一没有规定任何OutOfMemoryError的区域
Java虚拟机栈(线程的私有内存)
  • 生命周期和线程相同,是方法运行时的基础数据结构
  • 描述的是Java方法(也就是字节码)执行的内存模型:每个方法在执行的时候会创建一个栈帧(Stack Frame)
  • 栈帧:用来存储局部变量表,操作数栈,动态链接,方法出口等信息
  • 栈帧的入栈和出栈对应:一个方法从调用到执行完成的过程
  • 局部变量表:编译期的基本数据类型(boolean byte char short int float long double)对象引用等
  • 该区域的两种异常:StackOverflowError OutOfMemoryError
本地方法栈(线程的私有内存)
  • 生命周期和线程相同,是为Native方法服务
  • 可以由具体的虚拟机自由实现
  • 该区域的两种异常:StackOverflowError OutOfMemoryError
Java堆 (Java Heap) (所有线程共享的)
  • 虚拟机内存中最大的区域
  • 被所有线程共享的内存区域
  • 在虚拟机启动时候创建
  • 唯一的目的是存放对象实例:现在有发生变化?
  • Java 堆可以处于物理上不连续的内存空间,逻辑上连续就可以
  • 该区域的异常:OutOfMemoryError
方法区 (所有线程共享的)
  • 被所有线程共享的内存区域
  • 用于存储已经被虚拟机加载的类信息,常量,静态变量,即时编译后的代码等数据
  • 使用永久代来实现方法区,这样HotSpot的垃圾收集器可以像管理Java堆一样管理这部分内存(1.7以前)
  • 使用Native Memory来实现方法区 (1.7及以后)
  • 当方法区无法满足内存分配需求时,将抛出OutOfMemoryError
运行时常量池 (所有线程共享的)
  • 方法区的一部分
  • 在编译期时生成的各种字面量和符号引用,在类加载后进入方法区的常量池
  • 但是Java并没有要求常量一定只有编译期才能产生,也就是运行期间也可能将新的常量放入池中,例:String 的intern()

上面我们回顾了下内存模型每个区块的职责和功能;我们将从变量这个角度入手去分析。我们知道在Java的堆中我们存储着我应用程序中的大部分变量的内存地址,变量的创建和销毁都在期间发生;那么我们实际Java程序在运行执行的时候是怎么样去和变量做交互和计算的呢?这就是我们要分析的,Java的内存存储着Java应用的数据,Java虚拟机的执行引擎则负责执行各种指令和计算机底层做交互和计算;那么就需要有一套很好的机制去处理内存中存储的变量(后续我都会以变量来描述,以变量的视角来阐述整个过程)。我整理了一张图来展现这个过程:
在这里插入图片描述

  1. 当执行引擎发出需要读取变量的指令的时候,我们当前工作的线程会接收到这个指令;在主内存会执行一次read操作,把变量的值读取并将变量的值传递到工作内存中
  2. 这个时候工作内存会马上执行一次load操作,将变量的值存入工作内存的变量副本
  3. 接下来工作内存中执行use操作将变量副本的值传递给执行引擎
  4. 执行引擎会拿到变量后做应用程序中定义的操作,然后把最后的结果返回到工作内存,此时的工作内存中执行的操作是assign会把执行的结果重新赋值给变量副本
  5. 接下来工作中执行一次store操作,把工作内存的变量值传递到主内存
  6. 主内接收到后,立马执行write操作,把变量值存入变量

上面已经很清晰的分析了了整个变量在主内存和工作内存之间的传递过程,不过这边值得注意的是还有两个操作lock和unlock。
当我们的线程要向主内存读取值得时候,应当先去检测这个变量是否被锁定,如果被锁定必须等待所释放才能读取(或者说被这个线程占用)。上面描述了两个过程一个是锁定,一个是释放;锁定得操作正是由lock完成的,当请求线程获取没有被锁定的变量的时候,会对这个变量进行锁定,因为主内存是共享的,防止其他线程对这个变量操作导致不满足原子性(也就是线程不安全)。当线程完成对这个变量的使用后,会执行unlock操作,释放对变量的锁定。

思考
  • 在Java并发编程中为了保证线程安全会提到3个概念,原子性,可见性和有序性。如何处理这三个问题是很多猿哥猿姐们很头疼的问题,我认为之所以很头疼是因为对底层是如何保证原子性,可见性,有序性的理解不够透彻,停留在很抽象的理解上,就更不用谈使用了。上面线程的整个变量的读取使过程,就保证了对一次变量操作的原子性,并且每一个操作顺序是固定的,这样就保证了在本线程内的原子性和有序性。在Java中还有一个修饰变量的volatile,实际上如果理解了上面整个过程,就很容易来分析volatile的原理,其实就是上面过程的一种特殊规则实现volatile的语义,在后面的篇幅中我们回来分析。
  • 在java中,我们经常使用synchronized来保证线程的安全,实际上synchronized保证的是一个变量在同一个时刻只能被一个线程进行lock操作,上面的图和描述已经很清晰的展现了这个过程。换句话说Java内存模型通过read、load、 assign、use、store和write操作来直接保证的变量的原子性。
  • volatile,则是通过自身包含了禁止指令重排序的语义,来保证可见性的。
volatile的理解
特性

我们先来描述一下volatile的特性:

  • 被volatile修饰过的变量,保证此变量对所有线程的可见性,即当一条线程修改了这个变量的值,新值对其他线程来说是可以立即得知的,普通变量的值在线程之间传递需要通过主内来完成。对于普通变量的过程是这样的:例如,线程A修改一个普通变量的值,然后向主内存进行回写,另外一条线程B在线 程A回写完成了之后再从主内存进行读取操作,新变量值才会对线程B可见,其实通过上面的图也能分析出这个结论。
  • 被volatile修饰过的变量,保证了可见性但不保证原子性

分析:volatile能保证可见性是因为,当一个线程修改了这个变量的值后会立即刷新主内存的这个变量,另一个线程使用该变量时会先把主内存的值刷新到工作内存。这保证了一个线程修改变量的值,其他线程时可见的,但是由于Java的运算并非是原子操作所以被volatile修饰的变量不能保证原子性。上面阐述的可见性,我认为还只是对保证可见性的描述,真正的原理我们可以通过上面的图接着往下分析。

实际上volatile是Java内存模型间相互操作的特殊规则,普通的变量是严格遵守上面步骤的,然而volatile则有着不一样的规则,正式这种规则保证了变量在不同线程的可见性。

  • 假定T 表示一个线程,V和W分别表示两个volatile型变量,那么在进行read、load、use、assign、 store和write操作时需要满足如下规则:
  • 只有当线程T对变量V执行的前一个动作是load的时候,线程T才能对变量V执行use动 作;并且,只有当线程T对变量V执行的后一个动作是use的时候,线程T才能对变量V执行 load动作。线程T对变量V的use动作可以认为是和线程T对变量V的load、read动作相关联,必 须连续一起出现(这条规则要求在工作内存中,每次使用V前都必须先从主内存刷新最新的 值,用于保证能看见其他线程对变量V所做的修改后的值)。
  • 只有当线程T对变量V执行的前一个动作是assign的时候,线程T才能对变量V执行store动 作;并且,只有当线程T对变量V执行的后一个动作是store的时候,线程T才能对变量V执行 assign动作。线程T对变量V的assign动作可以认为是和线程T对变量V的store、write动作相关 联,必须连续一起出现(这条规则要求在工作内存中,每次修改V后

在这里插入图片描述

其实就是上面这个图描述的,在线程执行完对变量的操作后,assign store write必须是连续在一起执行的,这样的话就保证了线程修改了变量的值,能立即从工作内存中刷新到主内存;反过来,在其他线程读取这个变量的时候use load read动作必须是连续在一起执行,这样就保证了其他线程在读取这个变量时会立即从主内存中读取变量;这样就能看到其他线程修改的指个值。

小结

我认为要很深刻的理解Java的并发编程,必须先对Java虚拟机的内存模型和工作过程有比较深刻的理解,才能真正的对java并发的同步问题,安全问题有更深刻的理解。后续会不断的分享Java虚拟机相关的知识,以上是在阅读相关书籍之后自己的理解,有问题欢迎指正和交流。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值