Java 内存模型
物理机中的内存模型
在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。不同架构的机器可以有不通的内存模型,而Java虚拟机也有自己的内存模型
Java内存模型
Java Memory Model,简称JMM。JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的。如果我们要想深入了解Java并发编程,就要先理解好Java内存模型。
- JMM内存模型定义了多线程之间共享变量的可见性以及如何在需要的时候对共享变量进行同步。
- JMM的主要目标是定义程序中各个变量的访问规则。这里的变量包括了实例字段、静态字段、构成数组对象的元素,但不包括局部变量和方法参数。
- JMM是围绕着在并发过程中如何处理原子性、可见性和有序性这3个特征来建立的
硬件内存模型
现代计算机一般是多CPU、多核心,在CPU内部有一组CPU寄存器,也就是CPU的储存器。CPU操作寄存器的速度要比操作计算机主存快的多。在主存和CPU寄存器之间还存在一个CPU缓存,CPU操作CPU缓存的速度快于主存但慢于CPU寄存器。
计算机的主存也称作RAM,所有的CPU都能够访问主存,而且主存比上面提到的缓存和寄存器大很多。当一个CPU需要访问主存时,会先读取一部分主存数据到CPU缓存,进而在读取CPU缓存到寄存器。当CPU需要写数据到主存时,同样会先flush寄存器到CPU缓存,然后再在某些节点把缓存数据flush到主存
共享内存模型
Java线程间通信采用共享内存模型,这里提到的共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化
Java内存模型和硬件架构之间的桥接
正如上面讲到的,Java内存模型和硬件内存架构并不一致。硬件内存架构中并没有区分栈和堆,从硬件上看,不管是栈还是堆,大部分数据都会存到主存中,当然一部分栈和堆的数据也有可能会存到CPU寄存器中,如下图所示,Java内存模型和计算机硬件内存架构是一个交叉关系:
线程安全?
首先需要理解线程安全的两个方面:执行控制和内存可见。
执行控制 的目的是控制代码执行(顺序)及是否可以并发执行。
内存可见 控制的是线程执行结果在内存中对其它线程的可见性。根据Java内存模型的实现,线程在具体执行时,会先拷贝主存数据到线程本地(CPU缓存),操作完成后再把结果从线程本地刷到主存
定义:
- 当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。 《并发编程实战》p13
- 在本线程内所有操作都是有序的,在一个线程观察另外一个线程所有的操作都是无序的。《深入理解Java虚拟机 JVM高级特性与最佳实践》p374
volatile和synchronized
synchronized
volatile 关键字解决的是内存可见性的问题,会使得所有对volatile变量的读写都会直接刷到主存,即保证了变量的可见性。这样就能满足一些对变量可见性有要求而对读取顺序没有要求的需求。
【提问】是否使用了volatile就能保证线程安全?
使用volatile关键字仅能实现对原始变量(如boolen、 short 、int 、long等)操作的原子性,但需要特别注意, volatile不能保证复合操作的原子性,即使只是i++,实际上也是由多个原子操作组成:read i; inc; write i,假如多个线程同时执行i++,volatile只能保证他们操作的i是同一块内存,但依然可能出现写入脏数据的情况。
支撑Java内存模型的基础原理
指令重排序
在执行程序时,为了提高性能,编译器和处理器会对指令做重排序。
- 编译器优化重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序:如果不存l在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序:处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
内存屏障(Memory Barrier )
通过内存屏障可以禁止特定类型处理器的重排序,从而让程序按我们预想的流程去执行。内存屏障,又称内存栅栏,是一个CPU指令,基本上它是一条这样的指令:
- 保证特定操作的执行顺序。
- 影响某些数据(或则是某条指令的执行结果)的内存可见性。
- 编译器和CPU能够重排序指令,保证最终相同的结果,尝试优化性能。插入一条Memory Barrier会告诉编译器和CPU:不管什么指令都不能和这条Memory Barrier指令重排序,重排序时不能把后面的指令重排序到内存屏障之前的位置。
- Memory Barrier所做的另外一件事是强制刷出各种CPU cache,如一个Write-Barrier(写入屏障)将刷出所有在Barrier之前写入 cache 的数据,因此,任何CPU上的线程都能读取到这些数据的最新版本。
- 会增加一条 lock 操作指令,作用时使得本CPU的Cache写入内存,并且引起其他CPU或别的内核无效化其Cache,相当于对Cache中的变量做了一次store和write操作,可以让volatile变量的修改对其他CPU立即可见
JVM内存结构
运行时数据区域
1.程序计数器
程序计数器(Program Counter Register) 是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条执行字节码指令。每条线程都有一个独立的程序计数器。
2.Java虚拟机栈
线程私有,描述Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。一个方法对应一个栈帧
3.本地方法栈
和虚拟机栈类似,本地方法栈为Native方法服务。
4.Java堆
是Java虚拟机所管理的内存中最大的一块。由所有线程共享,在虚拟机启动时创建。堆区唯一目的就是存放对象实例。堆中可细分为新生代和老年代,再细分可分为Eden空间、From Survivor空间、To Survivor空间。
5.方法区
所有线程共享,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
6.运行时常量池
它是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项是常量池,用于存放编译期生成的各种字面量和符号引用
7.直接内存
虚拟机栈
栈帧
方法区
JVM性能监控与故障处理工具
检查Linux服务器性能
用十条命令在一分钟内检查Linux服务器性能-InfoQwww.infoq.cn
依据GC日志或GC频率
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -verbose:gc -Xloggc:[路径]
jstat -gc 458224
堆转储快照(heapdump/hprof文件)
jmap -dump:live,format=b,file=heapLive.hprof 2576
线程快照(threaddump)
Jstack –l 2576 > thread.txt
运行日志
access.log
异常堆栈