(1)请画出/描述出JVM的内存结构,并解释每个区域主要存储什么类型的数据,每个区域的作用是什么?
jvm将虚拟机分为5大区域,方法区、Java栈(虚拟机栈)、本地方法栈、堆、程序计数器。
程序计数器:线程私有的,是一块很小的内存空间,作为当前线程的下一条指令的地址;
Java栈(虚拟机栈):线程私有的,每个方法执行的时候都会创建一个栈帧,用于存储局部变量表、操作数、 动态链接和方法返回等信息,当线程请求的栈深度超过了虚拟机允许的最大深度时,就会抛出StackOverFlowError;
本地方法栈:线程私有的,保存的是native方法的信息,当一个jvm创建的线程要调用native方法时,jvm不会在虚拟机栈中为该线程创建栈帧,而是利用动态链接在操作系统层面调用该方法。(动态链接,可以让程序在运行时,加载和连接所需代码库或模块)
堆:是所有线程共享的一块内存,几乎所有对象的实例和数组都要在堆上分配内存,因此该区域是垃圾回收的重点区域。
方法区:是所有线程共享的一块内存。存放已被加载的类信息、常量、静态变量、编译后的代码数据。在jdk1.8及之后,不存在方法区了,被元数据区(即永久代)替代了。原方法区被分成两部分,1:加载的类信息;2:运行时常量池;加载的类信息被保存在元数据区中,运行时常量池保存在堆中。
(2)什么是Java内存模型(JMM)?
Java内存模型,也称为JMM,它在底层内存模型的基础上,定义了多线程编程的一组规则,来保证线程间的可见性。这组规则,就是Happens-before规则。
(3)怎么理解JMM的Happens-before规则?规则里包括哪些内容?
Happens-Before,JMM 规定,要想保证 B 操作能够看到 A 操作的结果(无论它们是否在同一个线程),那么 A 和 B 之间必须满足 Happens-Before 关系:
单线程规则:一个线程中的每个动作都 happens-before 该线程中后续的每个动作
监视器锁定规则:监视器的解锁动作 happens-before 后续对这个监视器的锁定动作
volatile 变量规则:对 volatile 字段的写入动作 happens-before 后续对这个字段的每个读取动作
线程 start 规则:线程 start()方法的执行 happens-before 一个启动线程内的任意动作
线程 join 规则:一个线程内的所有动作 happens-before 任意其它线程在该线程 join()成功返回之前
传递性规则:如果 A happens-before B, 且 B happens-before C, 那么 A happens-before C
总而言之,happens-before 也是为了保证可见性,比如解锁和加锁的动作,可以这样理解,线程1释放锁退出同步块,线程2加锁进入同步块,那么线程2就能看见线程1对共享对象修改的结果。
(4)JMM保证同步的关键字有哪些?
volatile:保证可见性、有序性,不保证原子性。会插入特定的内存屏障来禁止指令重排序。
synchronized:保证可见性、有序性、原子性。
final:通过禁止在构造函数初始化和给 final 字段赋值这两个动作的重排序,保证可见性。final字段的赋值总是在构造函数执行的最后一步完成,并且这个赋值操作与构造函数中前面的任何操作都是“串行”进行的,不会交错执行。这样就保证了在对象构造完成并发布给其它线程之前,final字段的值已经确定并且对其它线程可见。
拓展1:
this引用逃逸就不一定保证可见性。this引用逃逸是指在Java编程中,在对象构造阶段,构造函数内部的this引用在对象还未完全构造完成的情况下就被其它线程获取到了。
拓展2:
可见性:当一个线程对synchronized修饰的代码块或方法进行解锁时,将会把此块或方法内部对共享变量所做的修改刷新到主内存中。其它线程在随后对该synchronized块或方法进行加锁时,会从主内存中读取最新值,从而确保了数据的可见性。
有序性:synchronized确保在任意时刻,最多只有一个线程能够访问被它保护的代码块或方法,从而保证了在并发环境下对共享资源的访问是有序的。
原子性:synchronized通过对临界区的互斥访问,可以保证被保护的代码块或方法作为一个整体被执行,即一次只有一个线程能够执行这部分代码,因此在临界区内对共享变量的操作是原子的。(临界区,只能有一个线程进入并执行的代码区域)
指令重排序:synchronized代码块内部可能发生指令的重排序,虽然有可能进行重排序,后面的指令先执行,但是重排序的最终结果一定是和单线程顺序执行的最终结果相同。