常问面试题
- 请你谈谈你对JVM的理解? java—>class---->jvm
- Java8虚拟机和之前的变化更新?
- 什么是OOM 内存溢出
- 什么是栈溢出StackOverFlowError? 怎么分析
- -JVM 的常用调优参数?
- 内存快照如何抓取,怎么分析Dump文件?知道吗
- 谈谈JVM中,类加载器你的认识?
JVM概念
JVM是 Java Virtual Machine(Java虚拟机) 的缩写。
虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
简单来说JVM是用来解析和运行Java程序的。
Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。 Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够 “一次编译,到处运行” 的原因。
1. JVM的位置
三种JVM:
- Sun公司:HotSpot 用的最多 (HostSpot Java HotSpot™ 64-Bit Server VM (build 25.101-b13, mixed mode))
- BEA:JRockit
- IBM:J9VM
我们学习都是:HotSpot
2. JVM体系结构
jvm调优:99%都是在方法区和堆,大部分时间调堆。 JNI(java native interface)本地方法接口
3. 类加载器
作用:加载Class文件~ 如果new Student();(具体实例在堆里,引用变量名放栈里)
- 启动类(根)加载器:jvm自带加载器,负责java核心平台库,装载核心类库。
- 扩展类加载器
- 应用程序加载器
类加载器的作用:
类加载器的作用时把类(class)装载进内存。JVM规范定义了如下类型的类加载器。
4.双亲委派机制
Class Not Found 错误
- 第一步:类加载器收到类加载的请求
- 第二步:将这个请求向上委托给父类加载器去完成 ,一直向上委托,直到启动类加载器(Boot)
- 第三步:启动类加载器检查是否能够加载当前和这个类 ,能加载就结束,使用当前的加载器,否则,抛出异常,通知子加载器进行加载。
- 第四步:重复 第三步 步骤。
null : java调用不到 ~C 、C++
Java = C+±- : 去掉繁琐的东西,指针,内存管理。
双亲委派机制参考:https://www.jianshu.com/p/1e4011617650
概念: 当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。
例子: 当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。
作用:
- 防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
- 保证核心.class不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。 (比如重写一个String类,运行报错没有main方法)
比如:如果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。
总结:
5. 沙箱安全机制(了解)
组成沙箱的基本组件:
-
字节码校验器(bytecode verifier):确保Java类文件.Class遵循Java语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。
-
类装载器(class loader):其中类装载器在3个方面对Java沙箱起作用
- 它防止恶意代码去干涉善意的代码;//双亲委派机制
- 它守护了被信任的类库边界;
- 它将代码归入保护域,确定了代码可以进行哪些操作。
虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每一个被装载的类将有一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的,它们互相之间甚至不可见。
类装载器采用的机制是双亲委派模式。
- 从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用;
- 由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然无法生效。
-
存取控制器(access controller):存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。
-
安全管理器(security manager):是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。
-
安全软件包(security package):java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:
-
安全提供者
-
消息摘要
-
数字签名 keytools https(需要证书)
-
加密
-
鉴别
-
6. Native(重点)
Native是核心,能把面试官唬住的东西。
-
凡是带了native 关键字的,说明java的作用范围达不到了,得回去调用底层C语言的库!
-
凡是带了native 关键字的方法会进入本地方法栈,其它的是java栈
JNI:Java Native Interface(本地方法接口)
调用本地方法接口(JNI)作用:
- 扩展java的使用,融合不同的编程语言为java所用
- java诞生的初衷是融合C/C++程序,C、C++横行,想要立足,必须要有调用C、C++的程序~
它在内存区城中专门开辟了块标记区城: Native Method Stack
Native Method Stack(本地方法栈):
- 登记native 方法,在执行引擎(Execution Engine)执行的时候。通过JNI 加载 本地方法库(Native Libraies) 中的方法。
在企业级应用中少见,与硬件有关应用:java程序驱动打印机,系统管理生产设备等,掌握即可
7. PC寄存器(线程私有)
程序计数器: Program Counter Register:
每个线程都有一个程序计数器,是线程私有的,就是一个指针, 指向方法区中的方法字节码 ( 用来存储 指向下一条指令的地址, 也即将要执行的指令代码 ), 在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
8. 方法区(线程共享)
Method Area方法区:
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
静态变量(static)、常量(final)、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关。
如:static,final,,Class(类模板), 常量池
面试题:一张白纸,画出对象实例化过程的内存图。(主要是考你对JVM的理解)
类模板,常量池,引用的话进入左边,去引入真实的对象
9. 栈(线程私有)
-
栈:数据结构
程序=数据结构+算法:持续学习~
程序=框架+业务逻辑:吃饭~ -
栈:先进后出、后进先出:桶
队列:先进先出 ( FIFO ):管
喝多了吐就是栈,吃多了拉就是队列
为什么main() 先执行,最后结束? (因为一开始mian()先压入栈) -
栈:栈内存,主管程序的运行,生命周期和线程同步;
线程结束,栈内存也就是释放,对于栈来说,不存在垃圾回收问题
一旦线程结束,栈就Over! -
栈存放:8大基本类型+对象引用+实例的方法
栈运行原理:栈帧(局部变量表+操作数栈)每调用一个方法都有一个栈帧
栈满了 main()无法结束,会抛出错误:栈溢出 StackOverflowError
栈帧图:
栈 + 堆 + 方法区:交互关系
10. 堆(线程共享)
Heap:一个JVM只有一个堆内存,堆的大小是可以调节的,线程共享。
类加载器读取了类文件后,一般会把什么东西放到堆中?
对象实例、实例的变量,数组,保存所有引用类型的真实对象
新生区、老年区、永久区
堆内存细分3个区域:
- 新生区(伊甸园区) Young/new
- 养老区 old
- 永久区 Perm
GC垃圾回收,主要是在伊甸园区和养老区!
假设内存满了,报错OOM,堆内存不够!OutOfMemoryError:Java heap space
新生区(伊甸园+幸存者区*2)
- 类诞生和成长甚至死亡的地方;
- 伊甸园,所有对象都是在伊甸园区new出来的!
- 伊甸园满,轻GC清理,活下来的放入幸存者区(0/1);
- 伊甸园+幸存者区都满了,转移到养老区;
- 所有的都满了,重GC清理(Full GC),
- 重GC清理后还是无法存放,满了,报OOM。
真理:经过研究,99%的对象都是临时对象!直接被清理了(走不到老年区)
老年区
- 新生区剩下来的,轻GC杀不死了。
永久区
- 这个区域常驻内存,用来存放JDK自身携带的Class对象,Interface元数据,存储的是java运行时的一些环境或类信息,该区域不存在垃圾回收GC。关闭虚拟机就会释放这个内存。
- jdk1.6之前:永久代,常量池在方法区
jdk1.7:永久代,但是慢慢退化了(去永久代)常量池在堆中
jdk1.8之后:无永久代,常量池在元空间 - 在JDK8以后,永久存储区改了个名字---->元空间
元空间逻辑上存在,物理上并不存在。
元空间不在堆里,在电脑的直接内存上,不受jvm控制
一个启动类,加载了大量的第三方jar包。Tomcat部署了太多的应用,大量动态生成的反射类。不断的被加载。直到内存满,就会出现OOM。
方法区又称非堆(non-heap),本质还是堆,只是为了区分概念。
扩展:
一句话:常量池一直在方法区,其中的字符串池 JDK1.7之后保存到了堆中。
堆内存调优
public static void main(String[] args) {
//返回虚拟机试图使用的最大内存
long max = Runtime.getRuntime().maxMemory(); //字节 1024*1024
//返回jvm初始化的总内存
long total = Runtime.getRuntime().totalMemory();
System.out.println("max="+max+"字节\t"+(max/(double)1024/1024+"MB"));
System.out.println("total="+total+"字节\t"+(total/(double)1024/1024+"MB"));
/* 运行后:
max=1866465280字节 1780.0MB
total=126877696字节 121.0MB
*/
//默认情况下,分配的总内存占电脑内存1/4 初始化1/64
}
//面试题:报OOM怎么办?
/*
1.尝试扩大堆内存,如果还报错,说明有死循环代码 或垃圾代码
2.分析内存,看一下哪个地方有问题(专业工具)
*/
扩大内存方法:
Edit Configration>add VM option>输入:-Xms1024m -Xmx1024m -XX:+PrintGCDetails
-
-Xss:规定了每个线程虚拟机栈的大小,一般情况下,256k是足够的,此配置将会影响此进程中并发线程数的大小。
-
-Xms:表示初始化JAVA堆的大小及该进程刚创建出来的时候,他的专属JAVA堆的大小,一旦对象容量超过了JAVA堆的初始容量,JAVA堆将会自动扩容到-Xmx大小。
-
-Xmx:表示java堆可以扩展到的最大值,在很多情况下,通常将-Xms和-Xmx设置成一样的,因为当堆不够用而发生扩容时,会发生内存抖动影响程序运行时的稳定性。
再次运行:
新生区+养老区:305664K+699392K=1005056K = 981.5M
说明元空间物理并不存在。
例子2:死循环 OOM
//-Xms8m -Xmx8m -XX:+PrintGCDetails
public static void main(String[] args) {
String str = "kuangshensayjava";
while (true){
str += str + new Random().nextInt(888888888)+ new Random().nextInt(21_0000_0000);
}
}
Jprofiler
在一个项目中,突然出现了OOM故障,该如何排除,研究为什么出错~
- 能够看到代码第几行出错:内存快照分析工具,MAT,Jprofiler
- Debug,一行行分析代码!
MAT,Jprofiler作用:
- 分析Dump内存文件,快速定位内存泄漏;
- 获得堆中的数据
- 获得大的对象(大厂面试)
- …
//-Xms 设置初始化内存分配大小 默认1/64
//-Xmx 设置最大分配内存,默认1/4
//-XX:+PrintGCDetails 打印GC垃圾回收信息
//-XX:+HeapDumpOnOutOfMemoryError oom DUMP
//-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
public class Demo03 {
byte[] array = new byte[1*1024*1024]; //1m
public static void main(String[] args) {
ArrayList<Demo03> list = new ArrayList<>();
int count = 0;
try {
while (true){
list.add(new Demo03()); //不停地把创建对象放进列表
count = count + 1;
}
} catch (Exception e) {
System.out.println("count: "+count);
e.printStackTrace();
}
}
}
11. GC:垃圾回收
JVM在进行GC时,并不是对这三个区域统一回收。大部分时候回收的是新生代
- 新生代
- 幸存区
- 老年区
GC两种:轻GC,重GC (Full GC,全局GC)
关于GC面试题:
- JVM的内存模型和分区~详细到每个分区放什么?
- 堆里面的分区有哪些?Eden, from, to, 老年区,说说它们的特点!
- GC算法有哪些?怎么用的?标记清除法,标记整理,复制算法,分代收集法。引用计数法。
- 轻GC与重GC分别在什么时候发生?
(1)引用计数法
- 为每个对象设置一个计数器,计算使用次数,0的就清除。
- 一般JVM不用,大型项目对象太多了。
- 循环引用问题,两个对象相互引用,导致一直存在,不能被回收。
(2)复制算法
GC后的幸存者往空的幸存区中存放
-XX:MaxTenuringThreshold=15 设置进入老年代的存活次数条件
在幸存区活过15次就转移到养老区!
满了也会转移到养老区
- 好处:没有内存的碎片,内存效率高
- 坏处:浪费了内存空间(一个幸存区永远是空的);假设对象100%存活,复制成本很高。
- 复制算法最佳使用场景:对象存活度较低 的时候,新生区~。
(3)标记清除算法
存活对象进行标记,GC回收时,清除没有标记的对象
- 优点:不需要额外空间,优化了复制算法。
- 缺点:两次扫描,严重浪费时间,会产生内存碎片。
(4)标记压缩(标记整理):再优化
三部曲:标记–清除–压缩
标记清除压缩:再优化
- 每标记清除几次就压缩一次,或者内存碎片积累到一定程度就压缩。
(5)总结
-
内存效率(时间复杂度):复制算法>标记清除算法>标记压缩算法
-
内存整齐度:复制算法=标记压缩算法>标记清除算法
-
内存利用率:标记压缩算法=标记清除算法>复制算法
难道没有最优算法吗?
没有最优的方案,永远只是时间或者空间的权衡,而现在因为不缺空间,所以都是优先复制算法
答案:无,没有最好的算法,只有合适的算法(GC也被称为分代收集算法)。
- 年轻代:存活率低,用复制算法。
- 老年代:存活率高,区域大,用标记-清除-压缩。
参考和研究:《深入理解Java虚拟机》
12. GC收集器
12.1 新生代
(1)Serial 垃圾收集器(单线程、 复制算法)
最基本的垃圾收集器,使用复制算法,单线程,虽然收集垃圾时需要暂停其他所有的工作线程,但简单高效,是 java 虚拟机运行在Client 模式下默认的新生代垃圾收集器
- 在HotSpot虚拟机中,使用-XX:+UseSerialGC参数可以指定年轻代和老年代都使用串行收集器。等价于新生代用Serial GC,且老年代用Serial Old GC
(2)ParNew 垃圾收集器 (Serial的多线程版本、 复制算法)
是 Serial 收集器的多线程版本 ,除了多线程进行GC外,其他与Serial一样,默认开启和 CPU 数目相同的线程数 。是很多 java虚拟机运行在Server 模式下默认的新生代垃圾收集器。
- 可以通过选项"-XX:+UseParNewGC"手动指定使用ParNew收集器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代
- 这里的多线程指的是垃圾收集时,多线程并行,并不是垃圾收集与程序运行并行
- 收集垃圾时,也需要暂停其他所有工作线程,然后多线程收集垃圾。
- 单CPU环境下,因为线程切换,性能较差。
(3)Parallel Scavenge 垃圾收集器(多线程复制算法、高效)
关注程序的吞吐量,即吞吐量优先。主要适用于在后台运算而不需要太多交互的任务。 自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别。
-
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间) ; 吞吐量优先,意味着在单位时间内,STW的时间最短;与之相对应的 低延迟 就是暂停时间优先,尽可能让单次STW时间最短;这两个无法同时实现。
-
收集垃圾时,也需要暂停其他所有工作线程,然后多线程收集垃圾。
参数配置
-XX:+UseParallelGC 手动指定年轻代使用Parallel并行收集器执行内存回收任务。
-XX:+UseParallelOldGC 手动指定老年代都是使用并行回收收集器。
分别适用于新生代和老年代。默认jdk8是开启的。
上面两个参数,默认开启一个,另一个也会被开启。(互相激活)
-XX:ParallelGCThreads 设置年轻代并行收集器的线程数。一般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能。
-XX:MaxGCPauseMillis 设置垃圾收集器最大停顿时间(即STw的时间)。单位是毫秒。
为了尽可能地把停顿时间控制在MaxGCPauseMills以内,收集器在工作时会调整Java堆大小或者其他一些参数。
对于用户来讲,停顿时间越短体验越好。但是在服务器端,我们注重高并发,整体的吞吐量。所以服务器端适合Parallel,进行控制。
该参数使用需谨慎。
-XX:GCTimeRatio 垃圾收集时间占总时间的比例(=1/(N+1))。用于衡量吞吐量的大小。
取值范围(0, 100)。默认值99,也就是垃圾回收时间不超过1%。
与前一个-XX:MaxGCPauseMillis参数有一定矛盾性。暂停时间越长,Radio参数就容易超过设定的比例。
-XX:+UseAdaptivesizePolicy 设置Parallel Scavenge收集器具有自适应调节策略
在这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,已达到在堆大小、吞吐量和停顿时间之间的平衡点。
在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMills,让虚拟机自己完成调优工作。
12.2 老年代
(1)Serial Old 垃圾收集器(单线程、标记整理算法)
是Serial的老年代版本,收集垃圾时也需要暂停其他所有的工作线程。
- 是Client模式下默认的老年代垃圾收集器
- Server模式下,搭配新生代的Parallel Scavenge 收集器使用(在 JDK1.5 之前版本中)。同时也作为老年代中使用 CMS 收集器的后备垃圾收集方案(当CMS发生Concurrent Mode Failure)。
(2)Parallel Old 垃圾收集器(多线程、标记整理算法)
Parallel Scavenge的老年代版本
- 吞吐量优先,意味着在单位时间内,STW的时间最短;与之相对应的 低延迟 就是暂停时间优先,尽可能让单次STW时间最短;这两个无法同时实现。
- 若相同对于吞吐量要求较高,可以Parallel Scavenge搭配Parallel Old使用。
12.3 CMS垃圾收集器(多线程、标记清除算法)
如果用Seria和Parallel系列的垃圾收集器:在垃圾回收的时,用户线程都会完全停止,直至垃圾回收结束!
CMS的全称:Concurrent Mark Sweep,翻译过来是并发标记清除
-
用CMS对比上面的垃圾收集器(Seria和Parallel和parNew):它最大的不同点就是并发:在GC线程工作的时候,用户线程不会完全停止,用户线程在部分场景下与GC线程一起并发执行。
-
但是,要理解的是,无论是什么垃圾收集器,Stop The World是一定无法避免的!CMS只是在部分的GC场景下可以让GC线程与用户线程并发执行
-
CMS的设计目标是为了避免老年代 GC出现长时间的卡顿(Stop The World)
(1)GCRoots详解
在讲CMS的工作流程之前先讲下GCRoots是什么?
- 所谓"GCroots,或者说tracingGC的“根集合”就是一组必须活跃的引用。
- 基本思路就是通过一系列名为”GCRoots”的对象作为起始点,从这个被称为GC Roots的对象开始向下搜索,如果一个对象到GCRoots没有任何引用链相连时,则说明此对象不可用。也即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活,没有被遍历到的就自然被判定为死亡(需要垃圾回收)。
Java中可以作为GC Roots的对象:
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈中JNI(即一般说的native方法)中引用的对象
基本思路就是通过一系列名为"GC Roots"的对象作为起始点, 从这些节点开始向下搜索, 搜索所走过的路径称为引用链(Reference Chain), 当一个对象到GC Roots没有任何引用链相连时, 则证明此对象是不可用的.
从上图, reference1、reference2、reference3都是GC Roots, 可以看出:
reference1 -> 对象实例1;
reference2 -> 对象实例2;
reference3 -> 对象实例4 -> 对象实例6;
reference5 -> 对象实例3;
- 可以得出对象实例1、2、4、6都具有GC Roots可达性, 也就是存活对象, 不能被GC回收的对象.
- 而对于对象实例3、5虽然直接连通, 但并没有任何一个GC Roots与之相连, 这便是GC Roots不可达的对象, 也就是GC需要回收的垃圾对象。
(2)CMS的工作流程
四个步骤:初始标记、并发标记、重新标记、并发清除
-
初始标记:会标记GCRoots直接关联的对象,速度很快,因为没有向下追溯(只标记一层),需要暂停所有工作线程。
-
并发标记:并发执行,不停止用户线程,从GC Roots向下追溯,标记所有可达的对象,比较耗费时间的(需要追溯)。
-
重新标记:并发标记阶段用户线程继续运行,对象有可能发生变化,为了确保对象状态正确性,需要重新标记并暂停工作线程。
-
并发清除:并发执行,不停止用户线程,执行清除GCRoots不可达对象的任务。这个过程,还是有可能用户线程在不断产生垃圾,但只能留到下一次GC 进行处理了,产生的这些垃圾被叫做 “浮动垃圾”,结束后重置 CMS 算法相关的内部数据,为下一次 GC 循环做准备
13. JMM
- 什么是JMM? :百度
java内存模型 Java Memory Model
- 它干嘛的? 学习途径:官方,其他人的博客,教学视频
https://www.sohu.com/a/420276955_612370
作用:缓存一致性协议,用于定义数据读写的规则。
JMM定义了线程工作内存和主内存之间的抽象关系:线程之间的私有变量存储在主内存中, 每个线程都有一个私有的本地变量。
解决共享对象可见性这个问题:volilate
- 如何学会它?
- lock(锁定),作用于主内存中的变量,把变量标识为线程独占的状态。
- read(读取),作用于主内存的变量,把变量的值从主内存传输到线程的工作内存中,以便下一步的load操作使用。
- load(加载),作用于工作内存的变量,把read操作主存的变量放入到工作内存的变量副本中。
- use(使用),作用于工作内存的变量,把工作内存中的变量传输到执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
- assign(赋值),作用于工作内存的变量,它把一个从执行引擎中接受到的值赋值给工作内存的变量副本中,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作。
- store(存储),作用于工作内存的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用。
- write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
我再补充一下JMM对8种内存交互操作制定的规则吧:
-
不允许read、load、store、write操作之一单独出现,也就是read操作后必须load,store操作后必须write。
-
不允许线程丢弃他最近的assign操作,即工作内存中的变量数据改变了之后,必须告知主存。
-
不允许线程将没有assign的数据从工作内存同步到主内存。
-
一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过load和assign操作。
-
一个变量同一时间只能有一个线程对其进行lock操作。多次lock之后,必须执行相同次数unlock才可以解锁。
-
如果对一个变量进行lock操作,会清空所有工作内存中此变量的值。在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。
-
如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。
-
一个线程对一个变量进行unlock操作之前,必须先把此变量同步回主内存。
再补充一下JMM对8种内存交互操作制定的规则吧: -
不允许read、load、store、write操作之一单独出现,也就是read操作后必须load,store操作后必须write。
-
不允许线程丢弃他最近的assign操作,即工作内存中的变量数据改变了之后,必须告知主存。
-
不允许线程将没有assign的数据从工作内存同步到主内存。
-
一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过load和assign操作。
-
一个变量同一时间只能有一个线程对其进行lock操作。多次lock之后,必须执行相同次数unlock才可以解锁。
-
如果对一个变量进行lock操作,会清空所有工作内存中此变量的值。在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。
-
如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。
-
一个线程对一个变量进行unlock操作之前,必须先把此变量同步回主内存。