Zer0e's Blog

浅谈Java内存模型

字数统计: 1.8k阅读时长: 6 min
2020/09/13 Share

前言

想写一篇文章,由于涉及到Java内存的知识,就先来写写前置知识。那本篇就来说说JVM的基础,Java内存区域与Java内存模式(JMM)。
ps. 本篇基本上想到哪里说到哪里,并无什么先后顺序。

正文

Java内存区域

那我相信学习过JVM的同学基本上都知道JVM将运行时内存划分为几个区域。总体上可以分为两类:线程共享区域与线程私有区域。
其中线程共享区域分为方法区和堆。线程私有区域有虚拟机栈,本地方法栈还有程序计数器。

方法区(Method Area)

方法区中主要用于存储已经被虚拟机加载的类信息,常量还有一些静态变量。注意,方法区中有一个运行时常量池,用于存储编译器生成的各种字面量与符号引用,加载过后存放在此。

堆(Java Heap)

这个区域是我们常见的区域,主要是存放对象实例,几乎所有的对象实例都在这里分配内存,也是GC的主要区域。

虚拟机栈(JVM Stacks)

是线程私有的区域,与线程同时创建,每一个方法执行的时候都会创建一个叫做栈帧的东西来存储方法的变量表,动态链接方法,返回值等等信息,并存放在栈中,调用方法就是入栈,而调用结束就是出栈操作。

程序计数器(Program Counter Register)

在操作系统中我们也学习过这个概念。主要是代表当前线程所执行的字节码行号的指示器。运行代码时,通过改变计数器的值来选取下一条需要执行的字节码指令。

本地方法栈(Native Method Stacks)

主要是用到的Native方法相关的信息,一般来说无需关心。

主内存与工作内存

那由于JVM运行程序是以线程为单位,所以每个线程创建的时候JVM会为其创建一个工作内存,用于存储变量私有的数据,而JMM规定所有变量都存储在主内存,主内存是共享的,所有线程都能访问,因此对变量的操作是这样的,首先从主内存中拷贝变量到工作内存中,修改变量完成后再写回主内存中。所以线程共享的变量必须通过主内存来完成。

Java内存区域与JMM

这两个是不同的概念,JMM是一组规则,控制程序中各个变量在共享区域与私有区域的访问方式。他们的相似点在于都存在共享区域与私有区域,JMM中主内存属于共享区域,从内存模型来讲应该包括了方法区与堆,私有区域同理。

JMM如何保证原子性,可见性,有序性

那在Java当中我们要保证原子性可以使用锁来实现,工作内存与主内存的可见性可以使用synchronized关键字或者volatile关键字,而因为指令重排导致的有序性问题可以使用volatile关键字。
那仅靠synchronized和volatile关键字会使得代码变得十分繁琐。因此JMM还提供一个叫做happens-before的原则来保证三性。规则的内容如下:

  • 程序顺序原则,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
  • 锁规则 解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。
  • volatile规则 volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。
  • 线程启动规则 线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见
  • 传递性 A先于B ,B先于C 那么A必然先于C
  • 线程终止规则 线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。
  • 线程中断规则 对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。
  • 对象终结规则 对象的构造函数执行,结束先于finalize()方法

以上八条原则使得某些情况下需要任何同步就可达到预期效果。

volatile关键字

被volatile所修饰的变量对所有线程都是可见的,即对该变量的写操作都能立即反应到其他工作内存中。但这个关键字并不保证操作的原子性。因为对变量的修改往往需要两个步骤,读与更新,如果不能保证这个两个操作是同时进行,那么线程就是不安全的。
那volatile关键字的工作原理又是什么?其实就是线程在写一个volatile变量时,会把线程所对应的工作内存设置为无效的,那该线程只能从主内存中重新读取变量。其次就是禁止指令重排,之前在写单例模式的时候也提到过,对实例的赋值其实并不是我们预想的那样,例如instance = new A();,我们期待是先分配内存,然后初始化对象,最后进行赋值,那JVM可能会将该代码指令进行重排序,即先分配空间,然后赋值,最后再初始化对象。
那在单线程中是完全ok的,没什么影响,那在多线程中就可能出现一致性问题,也就是出现了线程安全问题。那禁止指令的重排,我们就可以使用volatile关键字来解决。

总结

本篇主要是简单讲讲Java的内存区域与内存模型(JMM),其中我们需要知道JMM其实就是一组规则,意图是解决并发编程中出现的线程安全问题,它提供了happen-before原则,并且我们还可以使用synchronized或volatile,来帮助我们实现多线程环境下的原子性,可见性,有序性。

CATALOG
  1. 1. 前言
  2. 2. 正文
    1. 2.1. Java内存区域
      1. 2.1.1. 方法区(Method Area)
      2. 2.1.2. 堆(Java Heap)
      3. 2.1.3. 虚拟机栈(JVM Stacks)
      4. 2.1.4. 程序计数器(Program Counter Register)
      5. 2.1.5. 本地方法栈(Native Method Stacks)
    2. 2.2. 主内存与工作内存
    3. 2.3. Java内存区域与JMM
    4. 2.4. JMM如何保证原子性,可见性,有序性
    5. 2.5. volatile关键字
  3. 3. 总结