Java内存模型
1.1、主内存和工作内存
Java内存模型规范了Java虚拟机和计算机内存是如何协同工作的,规定了一个线程何时和如何查看和修改其他线程共享的变量的值以及在必须时如何同步的访问共享变量。
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
1.2、内存可见交互操作
关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:
- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态
- unlock (锁定):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read (读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load (载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
- use (使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
- assign (赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量
- store (存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
- write (写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中
如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。也就是read和load之间,store和write之间是可以插入其他指令的,如对主内存中的变量a、b进行访问时,可能的顺序是read a,read b,load b, load a。
Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:
不允许read和load、store和write操作之一单独出现
不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
一个变量在同一时刻只允许一条线程对其进行lock操作,lock和unlock必须成对出现
如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。
1.3、重排序
在执行程序时为了提高性能,编译器和处理器经常会对指令进行重排序。重排序分成三种类型:
1、编译器优化的重排序。编译器在不改变单线程程序语义放入前提下,可以重新安排语句的执行顺序。
2、指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依
赖性,处理器可以改变语句对应机器指令的执行顺序。
3、内存系统的重排序。由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序
执行。
从Java源代码到最终实际执行的指令序列,会经过下面三种重排序:
为了保证内存的可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。Java内存模型把内存屏障分为LoadLoad、LoadStore、StoreLoad和StoreStore四种:
OOM
按理讲内存模型应该把JVM结构讲一下的,但是JVM结构已经烂大街饿了(主要还是懒),所以用一个面试题来回顾一下
学弟面美团时的一个问题:多线程中有一个线程OOM了,程序还会继续运行吗?第一反应是考察JVM结构,OOM意味着共享区域堆内存不足,既然堆内存不足,那么其他线程就也会受影响,当然后来写代码试验结果也是很打脸了。
public static void main(String[] args){
new Thread(()->{
List<byte[]> list = new ArrayList<byte[]>();
while(true){
System.out.println("我是线程1"+new Date().toString()+Thread.currentThread()+"==");
//byte[] b = new byte[1024*1024*1024*1024*1];
list.add(new byte[1024*1024*1024*1024*1]);
try {
Thread.sleep(1000);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}).start();
new Thread(()->{
while(true){
System.out.println("我是线程2"+new Date().toString()+Thread.currentThread()+"==");
}
}).start();
}
上面的代码run configuration之后,实际上当现场1抛出OOM之后,线程2还是继续在跑,原因是当线程程1发生OOM之后,GC会迅速清理掉堆的内存,从而尽可能减少对其他线程的影响。
还有一个就是常见的OOM发生的情况,这里总结一下:
- 上面提到的堆内存不足是最常见的OOM之一,原因可能是内心泄露,或者堆大小设置不合理,处理大量数据没有指定合适的堆大小或者JVM引用处理不及时,内存无法释放等。
- Java虚拟机栈和本地方法栈也会有OOM,比如一段程序不断的进行递归调用,没有退出条件,就会导致不断地进行压栈,这时,JVM会抛出StackOverFlowError,当JVM试图去扩展栈空间就会抛出OOM
- 老版本的JDK中,永久代大小是有限的,JVM对永久代垃圾回收非常不积极,所以当不断添加新类型时,永久代也会出现OOM
- 直接内存不足等