
JVM内存结构如下图:
其中,方法区和堆是所有线程共享的数据区域,虚拟机栈、程序计数器和本地方法栈是线程私有的。
方法区(Method Area):
方法区属于线程共享的内存区域,又称Non-Heap(非堆),主要用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,根据Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常。值得注意的是在方法区中存在一个叫运行时常量池(Runtime Constant Pool)的区域,它主要用于存放编译器生成的各种字面量和符号引用,这些内容将在类加载后存放到运行时常量池中,以便后续使用。
JVM堆(Java Heap):
Java 堆也是属于线程共享的内存区域,它在虚拟机启动时创建,是Java 虚拟机所管理的内存中最大的一块,主要用于存放对象实例,几乎所有的对象实例都在这里分配内存,注意Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做GC 堆,如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。
程序计数器(Program Counter Register):
属于线程私有的数据区域,是一小块内存空间,主要代表当前线程所执行的字节码行号指示器。字节码解释器工作时,通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
虚拟机栈(Java Virtual Machine Stacks):
属于线程私有的数据区域,与线程同时创建,总数与线程关联,代表Java方法执行的内存模型。每个方法执行时都会创建一个栈桢来存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。每个方法从调用直结束就对于一个栈桢在虚拟机栈中的入栈和出栈过程。
本地方法栈(Native Method Stacks):
本地方法栈属于线程私有的数据区域,这部分主要与虚拟机用到的 Native 方法相关,一般情况下,我们无需关心此区域。
Java内存模型本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
JMM与Java内存区域唯一相似点,都存在共享数据区域和私有数据区域,在JMM中主内存属于共享数据区域,从某个程度上讲应该包括了堆和方法区,而工作内存数据线程私有数据区域,从某个程度上讲则应该包括程序计数器、虚拟机栈以及本地方法栈。
JMM与Java内存区域的不同就在于,JMM描述的是一组规则,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式,JMM是围绕原子性,有序性、可见性展开的。
1、原子性原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。比如对于一个静态变量int x,两条线程同时对他赋值,线程A赋值为1,而线程B赋值为2,不管线程如何运行,最终x的值要么是1,要么是2,线程A和线程B间的操作是没有干扰的,这就是原子性操作,不可被中断的特点。
2、有序性有序性是指对于单线程的执行代码,我们总是认为代码的执行是按顺序依次执行的,这样的理解并没有毛病,毕竟对于单线程而言确实如此,但对于多线程环境,则可能出现乱序现象,因为程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未必一致,要明白的是,在Java程序中,倘若在本线程内,所有操作都视为有序行为,如果是多线程环境下,一个线程中观察另外一个线程,所有操作都是无序的,前半句指的是单线程内保证串行语义执行的一致性,后半句则指指令重排现象和工作内存与主内存同步延迟现象。
理解指令重排计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种:
其中编译器优化的重排属于编译期重排,指令并行的重排和内存系统的重排属于处理器重排,在多线程环境中,这些重排优化可能会导致程序出现内存可见性问题,下面分别阐明这两种重排优化可能带来的问题。
编译器重排:
线程 1 线程 2 1: x2 = a ; 3: x1 = b ; 2: b = 1; 4: a = 2 ; //因为如果编译器对这段程序代码执行重排优化后,可能出现下列情况 线程 1 线程 2 2: b = 1; 4: a = 2 ; 1:x2 = a ; 3: x1 = b ;
处理器指令重排:
先了解一下指令重排的概念,处理器指令重排是对CPU的性能优化,从指令的执行角度来说一条指令可以分为多个步骤完成,如下
CPU在工作时,需要将上述指令分为多个步骤依次执行(注意硬件不同有可能不一样),由于每一个步会使用到不同的硬件操作,比如取指时会只有PC寄存器和存储器,译码时会执行到指令寄存器组,执行时会执行ALU(算术逻辑单元)、写回时使用到寄存器组。为了提高硬件利用率,CPU指令是按流水线技术来执行的,如下:
指令1: IF ID EX MEM WB 指令2: IF ID EX MEM WB 指令2: IF ID EX MEM WB
虽然流水线技术可以大大提升CPU的性能,但不幸的是一旦出现流水中断,所有硬件设备将会进入一轮停顿期,当再次弥补中断点可能需要几个周期,这样性能损失也会很大。因此我们需要尽量阻止指令中断的情况,指令重排就是其中一种优化中断的手段,我们通过一个例子来阐明指令重排是如何阻止流水线技术中断的:
一段代码:
a = b + c ; d = e + f ;
上述代码在CPU执行的处理过程:
LW R1,b: IF ID EX MEM WB LW R2,c: IF ID EX MEM WB ADD R3,R1,R2: IF ID X EX MEM WB SW a,R3: IF X ID EX MEM WB LW R4,e: X IF ID EX MEM WB LW R5,f: IF ID EX MEM WB SUB R6,R4,R5: IF ID X EX MEM WB SW d,R6: IF X ID EX MEM WB
上述便是汇编指令的执行过程,在某些指令上存在X的标志,X代表中断的含义。这是因为部分数据还没准备好,如执行ADD指令时,需要使用到前面指令的数据R1,R2,而此时R2的MEM操作没有完成,即未拷贝到存储器中,这样加法计算就无法进行,必须等到MEM操作完成后才能执行,也就因此而停顿了,其他指令也是类似的情况。
停顿会造成CPU性能下降,因此我们应该想办法消除这些停顿,这时就需要使用到指令重排了。既然ADD指令需要等待,那我们就利用等待的时间做些别的事情,如把LW R4,e 和 LW R5,f 移动到前面执行,毕竟LW R4,e 和 LW R5,f执行并没有数据依赖关系,对他们有数据依赖关系的SUB R6,R5,R4指令在R4,R5加载完成后才执行的,没有影响,过程如下:
LW R1,b: IF ID EX MEM WB LW R2,c: IF ID EX MEM WB LW R4,e: IF ID EX MEM WB ADD R3,R1,R2: IF ID EX MEM WB LW R5,f: IF ID EX MEM WB SW a,R3: IF ID EX MEM WB SUB R6,R4,R5: IF ID EX MEM WB SW d,R6: IF ID EX MEM WB
所有的停顿都完美消除了,指令流水线也无需中断了,这样CPU的性能也能带来很好的提升,这就是处理器指令重排的作用。
3、可见性在多线程环境中,由于线程对共享变量的操作都是线程拷贝到各自的工作内存进行操作后才写回到主内存中的,这就可能存在一个线程A修改了共享变量x的值,还未写回主内存时,另外一个线程B又对主内存中同一个共享变量x进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题,另外指令重排以及编译器优化也可能导致可见性问题。
4、JMM的解决方案 依靠关键字:synchronized、ReentrantLock和volatile如原子性问题,可以使用synchronized关键字或者重入锁(ReentrantLock)保证程序执行的原子性;而工作内存与主内存同步延迟现象导致的可见性问题,可以使用synchronized关键字或者volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见;对于指令重排导致的可见性问题和有序性问题,则可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化,关于volatile稍后会进一步分析。
happens-before原则倘若在程序开发中,仅靠sychronized和volatile关键字来保证原子性、可见性以及有序性,那么编写并发程序可能会显得十分麻烦,幸运的是,在Java内存模型中,还提供了happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据,happens-before 原则内容如下:
volatile是Java虚拟机提供的轻量级的同步机制。volatile关键字有如下两个作用:
volatile的底层实现原理是内存屏障——Memory Barrier,又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执行顺序,二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。
1、volatile的可见性public void actor(I_Result r){
num=2;//num=2也同步到主存中
ready=true;//ready是volatile赋值的变量
//写屏障
}
public void actor(I_Result r){
//读屏障
//ready是volatile赋值的变量
//保证以下所有变量都是最新的
if(ready){
r.r1=num+num;
} else {
r.r1=1;
}
}
2、volatile的有序性
public void actor(I_Result r){
num=2;
ready=true;//ready是volatile赋值的变量
//写屏障
}
public void actor(I_Result r){
//读屏障
//ready是volatile赋值的变量
//保证以下所有变量都是最新的
if(ready){
r.r1=num+num;
} else {
r.r1=1;
}
}
注意:volatile不能解决指令交错!
public final class Singleton{
private Singleton(){}
private static Singleton INSTANCE=null;
public static Singleton getInstance(){
if(INSTANCE==null){
synchronized(Singleton.class){
if(INSTANCE==null){
INSTANCE=new Singleton();
}
}
}
return INSTANCE;
}
}
在多线程环境下,上面的代码是有问题的,getInstance()方法对应的字节码:
其中的重点部分:
也许 jvm 会优化为:先执行 24,再执行 21。如果两个线程 t1,t2 按如下时间序列执行:
这时t1还未完全将构造方法执行完毕,如果在构造方法中执行很多初始化操作,那么t2拿到的将会是一个未初始化完毕的单例。
解决方法:使用volatile修饰INSTANCE。
public final class Singleton{
private Singleton(){}
private static volatile Singleton INSTANCE=null;
public static Singleton getInstance(){
//由于禁止指令重排,只有实例没有被创建才会进入synchronized内部代码块
if(INSTANCE==null){
synchronized(Singleton.class){
if(INSTANCE==null){
INSTANCE=new Singleton();
}
}
}
return INSTANCE;
}
}
3、volatile不具备的原子性
public class VolatileVisibility {
public static volatile int i =0;
public static void increase(){
i++;
}
}
正如上述代码所示,i 变量的任何改变都会立马反应到其他线程中,但是如此存在多条线程同时调用increase()方法的话,就会出现线程安全问题,毕竟i++;操作并不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上1,分两步完成,如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同值的加1操作,这也就造成了线程安全失败。
因此对于increase方法必须使用synchronized修饰,以便保证线程安全,需要注意的是一旦使用synchronized修饰方法后,由于synchronized本身也具备与volatile相同的特性,即可见性,因此在这样种情况下就完全可以省去volatile修饰变量。
public class VolatileVisibility {
public static int i =0;
public synchronized static void increase(){
i++;
}
}
参考
黑马程序员视频-JUC并发编程教程
全面理解Java内存模型(JMM)及volatile关键字_zejian_的博客-CSDN博客_jmm内存模型