关于Java内存模型

前面的话

一般说到Java的内存模型,我一般会理解为两方面,GC的内存模型和并发的内存模型,不过GC方面更像是描述JVM的结构。

GC内存模型

这一块在《论jvm的优化》那篇中有做简单的介绍,不过那篇主要说了GC的算法和策略。

1、程序计数器
每一个线程都有一个单独的程序计数器,用于记录下一条执行的指令。各线程之间的程序计数器不会相互干扰。果当前线程正在执行一个Java方法,则程序计数器记录正在执行的Java字节码地址。

2、Java虚拟机栈
虚拟机栈也是Java线程私有的内存空间。它存放堆中对象的引用;方法调用的堆栈信息,和方法中的局部变量,参与方法的调用和返回。

JVM允许Java栈的大小是固定或者动态的,这里有两种异常是跟栈空间有关联的:StackOverflowErrorOutOfMemoryError,如果在线程计算的过程中,请求的栈深度大于最大可用的深度,则会抛出StackOverflowError,这一般会发生在递归调用中。如果栈的空间是动态扩展的,而扩展栈的过程中没有足够的内存空间来支持栈的发展,则会抛出OutOfMemoryError。JVM参数-Xss可以设置栈的大小,来改善这一现状。

虚拟机栈在运行时有一种数据结构叫栈帧,用于保存上下文数据,在栈帧中存放了方法的局部变量,返回地址等。如果方法调用时,方法的参数和局部变量较多,则相应的栈帧空间就会增大。所以说方法嵌套调用的次数和栈大小有关,栈越大,则嵌套调用次数越多。相反,一个方法的参数和局部变量越多,则栈帧也越大,嵌套调用次数就会减少。

3、本地方法栈
和Java虚拟机栈相似,它用于管理本地方法的调用。

4、Java堆
开发者分配的对象都在这,线程之间共享这片区域。一般分为Eden,Survivor1,Survivor2和old区。

5、方法区
被线程共享,存着类信息,方法的信息,和常量池。类加载是,就是将类的字节码文件数据加载到方法区生产类对象。因为有常量池,所以方法区又被称为永久代。
但这里面的对象其实也是可以被GC回收的。

并发内存模型(JMM)

主内存与工作内存

Java内存模型中规定了所有的变量都存储在主内存中,每条线程有自己单独的工作内存(有点类似于处理器的高速缓存)。线程的工作内存中存储着主存中对应变量的副本拷贝,一般情况下线程对变量的所有操作(读取,赋值)都必须在工作内存中进行,而不能直接读写在主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递必须得经过主内存。

支撑Java内存模型的基础原理

内存间交互操作happens-before
一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间涉及到8个操作:(说实话这里我感觉有点复杂,以后等理解深入后再详细解释这一块)

  • lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
  • unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
  • write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

重排序
在执行程序时,为了提高性能,编译器和处理器对指令做重排序。重排序分为三种类型:

  1. 编译器优化的重排序。编译器在不改变单线程程序语义放入前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序。由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

经过这三种重排序得到最终执行的指令序列。
这里又涉及到另一个概念内存屏障,为了保证内存的可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。
内存屏障具体到Java中有什么关系,Java内存模型中的volatile就是内存屏障实现的。如果一个变量是volatile修饰的,JMM会在写入这个字段之后插进一个Write-Barrier指令,并在读这个字段之前插入一个Read-Barrier指令。这意味着,如果写入一个volatile变量,可以保证:

  • 一个线程写入变量a后,任何线程访问该变量都会拿到最新值。
  • 在写入变量a之前的写入操作,其更新的数据对于其他线程也是可见的。因为Memory Barrier会刷出cache中的所有先前的写入。

happens-before
从jdk5开始,基于happens-before的概念来阐述操作之间的内存可见性。
在JMM中,如果一个操作的执行结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系,这个的两个操作既可以在同一个线程,也可以在不同的两个线程中。与程序员密切相关的happens-before规则如下:

  1. 程序顺序规则:一个线程中的每个操作,happens-before于该线程中任意的后续操作。
  2. 监视器锁规则:对一个锁的解锁操作,happens-before于随后对这个锁的加锁操作。
  3. volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读。
  4. 传递性规则:如果 A happens-before B,且 B happens-before C,那么A happens-before C。

注意:两个操作之间具有happens-before关系,并不意味前一个操作必须要在后一个操作之前执行!仅仅要求前一个操作的执行结果,对于后一个操作是可见的,且前一个操作按顺序排在后一个操作之前