前言
1、jvm 运行时数据区(运行原理)
2、Jvm分代回收分为哪几代
老年代 新生代
3、java 内存模型— 线程安全
一、 并发编程模型分类
在并发编程中中,我们通常需要处理2个关键问题:线程之间如何通信,如何同步。
通信: 是指线程怎么交换信息,线程之间通信机制有2种:共享内存,消息传递。
1)共享内存:线程之间通过写-读内存中的公共状态来隐式通信
2)消息传递:线程间没有公共状态,线程间通过明确的发送消息来显式通信。
同步: 是指程序控制不同线程之间操作发生相对顺序的机制,
1)共享内存:程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行,是显示进行的。
2)消息传递:消息的发送必须在消息的接收之前,因此同步是隐式进行的。
java的并发采用共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。
二、java内存模型的抽象
jvm内存模型就是一种规范定义,它定义了jvm在计算内存 中的工作方式
java内存模型规定:
1、 所有变量都存储在主内存(Main Memory)中。
2、 每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量对主内存的副本拷贝,
3、 线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。
4、 不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。
java内存模型(JMM)控制java之间的通信,JMM主要目标是定义程序中各个变量的访问规则,决定一个线程对共享变量的写入何时对另一个线程可见,它是java线程之间通信的控制机制,保证程序是正确同步的,程序的执行具有顺序一致性.
java 这样设计内存模型,那么在多线程的情况下会出现线程不安全的情况:在多线程之间修改 共享数据引起的
如何解决线程安全参见我的另一篇文章:https://mp.csdn.net/mdeditor/102644590#
三、java内存模型特征
Java内存模型是围绕着并发编程中原子性、可见性、有序性这三个特征来建立的,那我们依次看一下这三个特征:
1 原子性
原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。
可以使用synchronized关键字或者重入锁(ReentrantLock)保证程序执行的原子性
2 可见性
可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值。
JMM用volatile ,synchronized,Lock来实现可见性
3有序性
1)java提供volatile和synchronized来保证多线程之间操作的有序性,volatile关键字通过加入内存屏障来禁止指令的重排序,而synchronized关键字通过一个变量在同一时间只允许有一个线程对其进行加锁的规则来实现
2)java规定的happens-before原则,保证有序性
happens-before原则:
- 程序顺序原则,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
- 锁规则 解锁(unlock)操作先于后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。
- 线程启动规则 线程的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()方法
四、java运行时数据区域
jvm运行时数据区包含以下5部分
1、程序计数器(线程私有): 记录正在执行的虚拟机字节码的地址;
2、虚拟机栈(线程私有): 方法执行的内存区,每个方法执行时会在虚拟机栈中创建栈帧(存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。每个方法从调用直结束就对于一个栈桢在虚拟机栈中的入栈和出栈过程);
栈帧包括:
- 1、 局部变量表
- 2、 操作栈,操作栈元素的数据类型必须与字节码指令序列严格匹配
- 3、 动态连接, 指向运行时常量池中该栈帧所属方法的引用,为了 动态连接使用。
- 4、 返回地址, 正常退出,将返回值传递给调用者
- 5、 额外附加信息,由具体虚拟机实现
异常:
StackOverFlowError:当线程请求栈深度超出虚拟机栈所允许的深度时抛出
OutOfMemoryError:当Java虚拟机动态扩展到无法申请足够内存时抛出
3、本地方法栈(线程私有): 虚拟机的Native方法执行的内存区;
异常(Exception):Java虚拟机规范规定该区域可抛StackOverFlowError和OutOfMemoryError。
4、Java堆(线程共享): 对象分配内存的区域,主要存放对象实例,注意Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做GC 堆,如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。;
异常(Exception): Java虚拟机规范规定该区域可抛出OutOfMemoryError。
5、方法区(线程共享) :存放类信息、常量、静态变量、编译器编译后的代码等数据;
运行时常量池:存放编译器生成的各种字面量和符号引用,是方法区的一部分。
字面量: 比如我们定 int a = 1 ,string str = “abc”,那么1 和 abc就是字面量
符号引用: 比如我们main中调用一个ToolUnit.getRandom()方法,那么这个调用方法会用一个符号引用来表示
异常(Exception): Java虚拟机规范规定该区域可抛出OutOfMemoryError。
jvm运行时数据区为什么要划分成这5个区域?
以下一个简单的方法为例说明
public class DemoTest {
public int count(int x){
int y = 1;
int z = x+y;
return z;
}
public static void main(String[] args) {
DemoTest test = new DemoTest();
int resutl = test.count(1);
}
}
1、首先.java 文件被编译成.class,这个文件需要被存入到jvm中,那么jvm需要分配置一块内存放class信息那么就产生了方法区
2、执行main方法=》 main(DemoTest()->count()),执行代码中需要执行的方法,那么这些方法需要存放一区域产生了jvm栈
,jvm堆中存放的就是栈帧,首先main方法入栈-> 发现要执行count方法-> count方法入栈,count方法执行完就出栈,接着继续执行未执行完的main方法,所以方法存储的数据结构是栈(先进后出),每个线程都会执行方法,所以jvm栈是线程独有的。
2.1、执行count方法过程中,定义了一些变量、产生了一些操作,那么这些变量操作也需要地方存储,那么栈桢又细分成局部变量表、操作数栈
分别用来存储方法中的局部变量,操作数。
3、main方法中定义了一个实例变量,实例变量需要存储所以产生了java堆
,这个是线程共享的
4、方法实际上执行的每一条指令,那么执行当前指令以及下条指令,就产生了程序计数器
,通过改变程序计算器指向地址来执行下条指令,每个线程都需执行指令,所以程序计数器是线程私有的
5、同时jvm会跟操作系统打交道,所以执行的本地方法存在本地方法栈
,同不线程都可会执行本地方法栈,所以本地方法栈也是线程私有的。
线程独有的会在线程销毁的时候自动释放,而共享的这部分内存区域就需要gc来释放
内存管理分为内存分配和内存释放,看一下上面的五个内存区域,其实可以大致分为两部分,一部分是全局共享,一部分是线程独有。
线程独有:
对于线程独有这部分内存,都是随着线程的启动而创建,而当线程被销毁时,内存也就随之释放。这一部分内存,不需要垃圾搜集器的管理,而是JAVA虚拟机来主动管理,每当一个线程被创建的时候,JAVA虚拟机就会为其分配相应的PC寄存器和JAVA虚拟机栈,如果需要的话,还会有本地方法栈。相应的,当一个线程被销毁的时候,JAVA虚拟机也会将这个线程所占有的内存全部释放。
线程共享:
对线程共享的内存由GC管理。