JVM探究
请你谈谈你对JVM的理解?
- 用到了 桥接模式
java8虚拟机和之前的变化更新?
什么是OOM,什么是栈溢出StackOverFlowError?怎么分析?jprofiler
JVM的常用调优参数有哪些?
- 扩大运行时 内存
内存快照如何抓取,怎么分析Dump文件?知道吗?
谈谈JVM中,类加载器你的认识?
- rt-jar ext applicatoin
1.JVM的位置
2.JVM的体系结构
3.类加载器
4.双亲委派机制
5.沙箱安全机制
6.Native
7.PC寄存器
8.方法区
9.栈
10.三种JVM
Java HotSpot(TM)
英
/ˈhɒtspɒt/
n.
热点;热区
JVM在它的生存周期中有一个明确的任务,那就是运行Java程序,
- 因此当Java程序启动的时候,就产生JVM的一个实例;
- 当程序运行结束的时候,该实例也跟着消失了
11.堆
12.新生区、老年区
13.永久区
14.堆内存调优
15.Gc
-
复制
-
标记
-
标记压缩
-
标记清除
16.JMM
JVM的位置
- JVM是 C 写的
- JRE 包含了 JVM
java——>Class File——>类加载器 Class Loader
——>运行时数据区 Runtime Data Area 包含下面的:
- java栈 Stack
- 本地方法栈 Native Method Stack
- 堆 Heap
- 方法区 Method Area 特殊的堆
- 程序计数器 Program Counter,PC
——>到 下一层:
- 执行引擎 execution engine
- 垃圾回收器
- interpreter(解释器)
- JIT编译器
- 探测器
- 中间代码生成器
- 代码优化器
- 目标代码生成器
- (本地方法栈调用)——本地方法接口 Native Method
lnterface (java本地接口 JNI)——本地方法库(Native Method Library)- 线程的 native strat0(); 需要调用本地方法。操作系统的,C写的吧
-
栈 是调用了一个方法,调用完毕。就弹出了。
-
调优99%是在 堆里调。堆里才有垃圾回收。其他没有。
- 如:lombok
- 类加载器,可以做手脚(增加钩子函数)
- 大多都是在 执行引擎上。
类加载器
作用:加载Class文件
-
生成 对象的 Class文件 和 Class类
-
s=new Student() ;
- 引用放在 栈里。具体的 人,放在堆里。
//类是模板 是抽象的,对象是具体的,是对 抽象的实例化。
//1. 实用类 拿到class
class<car> carClass = car.class;
//2. 对象 变 class
car car1 = new Car();
class<? extends car> aclass = car1.getclass();
//class 变为 类加载器
ClassLoader classLoader = aclass.getclassLoader();
//1,先在 应用程序加载器 去找。
//sun.misc.Launcher$AppclassLoaderi@18b4aac2
system.out.printl1n(classLoader);
//2,在从:扩展类加载器
//sun.misc.Launcher$ExtClassLoader@677327b6
System.out.println(classLoader.getParent());
//null,获取不到,可能C写的
//System.out.print1n(classLoader.getParent().getParent());
//java = c++ :去掉繁琐的东西,指针,内存管理 c++--
- rt.jar
- java
- lang
- String
- ClassLoader 是抽象类,具体实现类,为:AppclassLoader
- lang
- java
- jdk8——jre——lib
- ——rt.jar
- lib下有:ext文件夹。这里是 扩展加载器。
1.虚拟机自带的加载器
⒉.启动类(根)加载器
3.扩展类加载器
4.应用程序(系统类)加载器
类加载器 Class Loader SubSystem(类加载子系统)
- 加载 Loading
- 连接 Linking 将类的二进制数据合并到JRE中
- verify 验证
- 字节流中包含的信息符合当前虚拟机的要求
- 随意打开一个 .class文件。确实是:cafe babe 开头
- -Xverify:none 参数来关闭大部分的类验证措施
- Prepare 准备
- JVM会在该阶段对静态变量,分配内存并默认初始化(对应数据类型的默认初始值
- resolve 解析
- 虚拟机将常量池内的符号引用替换为直接引用的过程
- 到了 解析后,就装入内存了,就是 直接引用了
- verify 验证
- 初始化 lnitialization
- 静态变量的赋值动作和静态代码块中的语句,并进行合并。
- 保证一个类的)方法在多线程环境中被正确地加锁、同步
双亲委派机制
//双亲委派机制:保证安全
// 1.APP-->EXC--B0OT(最终执行)。启动类(根)加载器 BootStrap
//只有 扩展类 + 根加载器 都没有的时候,才会执行 应用程序加载器的。如 Student类,toString();
//java.lang包。自己写String
public class String {
@Override
public String toString() {
return "hello";
}
//错误: 在类 java.lang.String 中找不到 main 方法
public static void main(String[] args) {
String s=new String();
s.toString();
}
}
1.类加载器收到类加载的请求
2.将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类加载器
3.启动加载器检查是否能够加载当前这个类,能加载就结束,使用当前的加载器,否则,抛出异常,通知子加载器进行加载
4.重复步骤:
- 最终都没有找到:class Not Found
沙箱安全机制
Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将jJava代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么? CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
所有的Java程序运行都可以指定沙箱,可以定制安全策略。
在Java中将执行程序分成本地代码和远程代码两种,**本地代码默认视为可信任的,而远程代码则被看作是不受信的。**对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱(Sandbox)机制。如下图所示JDK1.0安全模型
但如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。因此在后续的Java1.1版本中,针对安全机制做了改进,增加了安全策略,允许用户指定代码对本地资源的访问权限。如下图所示JDK1.1安全模型
- 增加了 受信任 的机制。收信任后,才可运行 远程代码。
在Java1.2版本中,再次改进了安全机制,增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。如下图所示JDK1.2安全模型
**当前最新的安全机制实现,则引入了域(Domain)的概念。**虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域(Protected Domain),对应不一样的权限(Permission)。存在于不同域中的类文件就具有了当前域的全部权限,如下图所示最新的安全模型(jdk 1.6)
组成沙箱的基本组件:
·`字节码校验器(bytecode verifier)︰确保Java类文件遵循Java语言规范。这样可以帮助ava程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。
- 比如 每行结束,少写了一个 分号。
·类装载器(class loader) :其中类装载器在3个方面对Java沙箱起作用
。它防止恶意代码去干涉善意的代码; //双亲委派
。它守护了被信任的类库边界;//双亲委派
。它将代码归入保护域,确定了代码可以进行哪些操作。//沙箱安全
- 比如:自己写的代码,不能调用 C的库。
虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每一个被装载的类将有一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的,它们互相之间甚至不可见。
类装载器采用的机制是双亲委派模式。
1.从最内层VM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用;
2.由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然无法生效。
·存取控制器(access controller)︰存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。
- java中 Robot类,模拟键盘 鼠标操作。
·安全管理器(security manager)∶是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。
·安全软件包(security package) : java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:
-
安全提供者
-
消息摘要。
-
数字签名 keytools
-
加密
-
鉴别
Native
- Java诞生的时候C、C++横行,想要立足,必须要有调用C、C++的程厅
new Thread(() -> {
}, "线程1").start();
//native :凡是带了native 关键字的,说明java的作用范围达不到了,回去调用底层c语言的库!
//即:进入本地方法栈,调用本地方法接口
//JNI,java native interface 作用:扩展ava的使用,融合不同的编程语言为Java所用!
//它在内存区域中专门开辟了一块标记区域:Native Method Stack,登记 native 方法
//在最终执行的时候,加载本地方法库中的方法
private native void starte();
//java 控制打印机,操作 Robot类等,才用 native
//现在调用其他接口:
Socket..webService~..http~
public class Thread implements Runnable {
//线程 start 源码
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
private native void start0();
JNl: Java Native Interface (Java本地方法接口)
凡是带了native关键字的方法就会进入本地方法栈,其他的就是Java栈:l
Native lnterface本地接口
本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序,Java在诞生的时候是C/C++横行的时候,想要立足,必须有调用C、C++的程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是在Native Method Stack中登记native方法,在(Execution Engine )执行引擎执行的时候加载Native Libraies。
目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者]ava系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间通信很发达,比如可以使用Socket通信,也可以使用Web Service等等,不多做介绍!
PC 寄存器
PC寄存器
程序计数器:Program Counter Register
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向像一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计
栈
1.栈︰数据结构
-
程序=数据结构+算法
-
程序=框架+业务逻辑:吃饭~
-
栈:先进后出、后进先出。
-
队列:先进先出( FIFO : First Input First Output )
-
喝多了吐就是栈,吃多了拉就是队列
main入栈——test方法入栈——test弹栈——main弹栈
- test调用a,a调用test,就是无限压栈。栈溢出。
Exception in thread "main" java.lang.stackOverflowError
栈:栈内存,主管程序的运行,生命周期和线程同步;
线程结束,栈内存也就是释放,对于栈来说,不存在垃圾回收问题一旦线程结束,栈就Over!
- 栈:8大基本类型+对象引用+实例的方法
栈运行原理:栈帧
栈包含:
- 栈顶
- 方法索引 (index)
- 输入输出参数
- 本地变量
- Class File: 引用(student s 会引用 这个类的 class,当然 更会引用 new student() )
- 父帧
- 子帧
- 栈底
程序正在执行的方法,一定在栈的顶部~
栈的内容有:
- 本地栈
- 引用
- 创建对象 Student S
- 具体的 =new Student()在 堆里。如果有常量,指向 常量池。
- 创建对象 Student S
- 基本类型,如 double
三种JVM
- ① Sun公司的HotSpot ,平时用
- ② BEA公司的JRockit
- ③ IBM公司的J9 JVM
- j9vm JIT
spot
英
/spɒt/
n.
地点,场所;(人体的)部位,地方;有某种特质的部分;点,斑点;污点,污渍;
v.
看见,注意到;(对比赛对手)让分,让步;飘了几滴雨,下小雨;发现,挖掘(某人有天赋)
rocket
英
/ˈrɒkɪt/
n.
火箭;火箭发动机
v.
快速增长,猛增;迅速移动,飞速行进
Rockit 尽情摇摆
堆
Heap,一个JVM只有一个堆内存,堆内存的大小是可以调节的。
类加载器读取了类文件后,一般会把什么东西放到堆中?︰类,方法,变量~,保存我们所有引用类型的真实对象;
新生区 老年区 元空间
堆内存中还要细分为三个区域:
-
新生区(伊甸园区 Eden Space)Young/New·
- **(伊甸园区),**student对象 第一次没有被回收,进入幸存区
-
幸存区0区
- 幸存区1区
-
类:诞生和成长的地方,甚至死亡;
-
伊甸园,所有的对象都是在伊甸园区new出来的!
-
假如只能放10个对象,满了 就出发一次 轻GC
- 清理后,活下来的,进入 幸存区
-
幸存者区(0,1)
-
第一次 轻GC是 清理 伊甸园区的,假如 伊甸园和幸存都满了。
-
触发重GC,清理 伊甸园和幸存区,活下来的 进入养老区。
-
-
**养老区old 老年代 **
- 幸存区 一直未 回收,比如student 超过了15次未回收,进入养老区。
-
**永久区Perm 元空间 持久代 **
-
在JDK8以后,永久存储区改了个名字(元空间)
-
方法区在这里。
-
这个区域常驻内存的。用来存放JDK自身携带的Class对象。Interface元数据
-
存储的是Jala运行时的一些环境或类信息,这个区域不存在垃圾回收~
-
关闭VM虚拟就会释放这个区域的内存~
-
方法区 是所有对象共享的。在堆里,叫非堆。
- 所以有些人的理解 堆只包含 新生代 和 老年代(不含 元空间)
- 方法区如下讲解存:static 静态变量、final 常量、(class类模板) 类信息、常量池
-
元空间 逻辑上存在,物理上 不存在。
-
perpetual
英
/pəˈpetʃuəl/
adj.
长期的,永恒的;连续不断的,重复不停的;(职位、工作或战利品)终身的;(投资)永不还本的;(植物)四季开花的,四季结果的
方法区 常量池
常量池在哪?
jdk1.6之前:永久代,常量池是在方法区;
jdk1.7:永久代,但是慢慢的退化了,去永久代,常量池在堆中
jdk1.8之后:无永久代,常量池在元空间
Method Area方法区
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
重要 重要 重要:
static 静态变量、final 常量、(class类模板) 类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关
- 静态变量
- 类模板
- 常量池
- 常量
public class Car {
private String name = "宝马";//如果 不赋值,就 常量池里拿。
}
堆内存 内存溢出 调优
Runtime.getRuntime().maxMemory();//使用-Xmx10m 调节。最大分配内存。默认为1/4。
Runtime.getRuntime().totalMemory();//使用-Xms5m调节。初始化内存分配大小。默认1/64
-Xms1024m -Xmx1024m -XX:+PrintGCDetails ///打印GC垃圾回收
-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError //oom DUMP 堆dump在 内存溢出错误
Dump
v.
丢弃,扔掉;乱堆,乱放;丢下,抛弃;分手,甩掉;倾销,抛售;转储,转存(计算机数据);(尤指系统发生故障后内存的)打印输出,卸出
n.
垃圾场;废料堆场
//返回虚拟机试图使用的最大内存
long max = Runtime.getRuntime().maxMemory();
//返回jvm的初始化总内存
long total = Runtime.getRuntime().totalMemory();
//默认情况下:分配的总内存是电脑内存的1/4,而初始化的内存:1/64
//我的为:10891.0MB。如果用47G/4=约11,752
System.out.println("最大" + (max / (double) 1024 / 1024) + "MB");
//我的为:734.5 * 64 = 47,008
System.out.println("最大" + (total / (double) 1024 / 1024) + "MB");
//报OOM Out OfMemory,第一步扩大 堆内存。第二步,用工具分析内存。
//VM options 调整:-Xms1024m -Xmx1024m -XX:+PrintGCDetails
//运行时配置 构建并运行————修改选项————添加VM选项
//PrintGCDetails打印后:YoungGen很多(eden占8% from to),OldGen很多,Metaspace
//from是 幸存区0区,to 幸存区1区。下次换个位置:1区 to 0区。
//新生代 + 老年代 = totalMemory。上面调成了1G。得出:元空间 逻辑上存在,物理上 不存在。
OOM排错
元空间:逻辑上存在︰物理上不存在
在一个项目中,突然出现了OOM故障,那么该如何排除.研究为什么出错·
能够看到代码第几行出错:内存快照分析工具,MAT,Jprofiler
-
MAT 是 Eclipse的
-
Dubug,一行行分析代码!
MAT,Jprofiler 作用:
- 分析Dump内存文件,
- 快速定位内存泄露;获得堆中的数据
- 获得大的对象~
Throwable(可抛出) 异常顶级类,包含:
- Exception 异常
- error 错误
Jprofiler
英
/ˈprəʊfaɪlə(r)
n.
分析器,分析工具;仿形铣床;[测] 断面仪
public class Car {
byte[] arr = new byte[1 * 1024 * 1024];//1M
public static void main(String[] args) {
//dump内存:
//使用 ArrayList循环加入,造成内存溢出。使用下面配置虚拟机。
//-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
//即可出现 dump文件。在本项目的src下出现:java_pid17500.hprof
ArrayList a = new ArrayList();
int count = 0;
try {
while (true) {
a.add(new Car());
count++;
}
} catch (Error e) {
System.out.println("count次数" + count);
e.printStackTrace();
}
//1. idea插件安装:Jprofiler,然后再安装 Jprofiler的客户端,可百度下载80M左右,但还要下载几百兆。
//2. 配置idea的Jprofiler,工具类JProfiler executable: 选择到 客户端安装的bin目录。
//之后用 Jprofiler的客户端 打开:java_pid17500.hprof,
//选择 大对象,即可看到 占用内存的对象。选择线程,即可看到 有多少个线程。
}
}
GC
轻重GC
垃圾回收:
-
GC垃圾回收,主要是在伊甸园区和养老区
- 作用区为:堆 Heap (当然堆里有 元空间 里有 方法区)
-
真理:经过研究,99%的对象都是临时对象!
-
新生区 养老区都满了。OOM,堆内存不够!
- 一个启动类,加载了大量的第三方jar包。
- Tomcat部署了太多的应用。
- 大量动态生成的反射类。
String s = "1";
while (true) {
s += s + new Random().nextInt(99999);
}
//Exception in thread "main" java.lang.OutOfNemoryError: Java heap space
//把拼接的死循环 自加字符串的,调成 8M,看到:GC 和 FULL GC。
// 4次轻GC,伊甸园+幸存都满了,走了一次重GC,之后又可以走一次轻GC,直到 0K-0K,彻底满了。
//年轻代,老年代,元空间也走不动了。都满了:java.lang.outOfMemoryrror: Java heap space
garbage collection
Garbage
英
/ˈɡɑːbɪdʒ
n.
<美>垃圾,废物;<美>垃圾箱;<非正式>废话,无聊的东西;无用信息
JVM在进行GC时,并不是对这三个区域统一回收。大部分时候,回收都是新生代~
-
新生代
-
幸存区
-
from to 。就是 0区 和 1区。换了位置,名字也换。第一个为from。
-
老年区
GC两种类:
- 轻量级(轻GC,针对新生区),普通的GC
- 重量级。重GC。Full GC,全局GC
- 伊甸园+幸存都满了,走了一次重GC
- 所有的都要清一遍,含 老年区。
常见问题2
-
JVM的内存模型和分区~详细到每个区放什么?
-
堆里面的分区有哪些?说说他们的特点!
- Eden,form,to,(新生区),老年区,
- GC的算法有哪些?
标记清除法,标记压缩(整理),复制算法,引用计数器
- 轻GC和重GC分别在什么时候发生?
1. 引用计数器
引用计数器:用的比较少
- 使用一个 计数器,默认为0,每个对象使用1次 +1。
- 清理 对象使用0次的。
- 计数器本身 也会有 消耗。
2. 复制算法
- 年轻代 主要用 复制算法。
1.每次GC都会将 Eden活的对象 移到幸存区中:
- 每次垃圾回收,清理完后:伊甸园区和 to区都是空的。
- 因为:原来 from中的数据,进入了 to区。伊甸园的存活的进入了to区。
- 垃圾回收后,已经没有 数据的from去,转变 成了 to区。
谁空谁是to 区
-
假如 各有1个。会把 1个 复制到 另一个区域。
-
此时 空的区域 变成 to,就是保证 to 区,永远干净。
当 对象经历 15次 GC,还没有死的时候,进入 养老区。
-xX: -XX:MaxTenuringThreshold=5 //通过这个参数可以设定进入老年代的时间
好处:没有内存的碎片~·
坏处:浪费了内存空间~ (浪费了 一个幸存区的空间,to区 为空)
- 假设对象100%存活(极端情况),from区 都要复制到 to区。
复制算法最佳使用场景:对象存活度较低的时候;新生区~
3.标记清除算法 和 4.压缩
扫描这些对象:对或者对象进行标记
清除:对没有标记的对象,进行清除
·优点:不需要额外的空间!
·缺点:两次扫描,严重浪费时间,会产生内存碎片。
标记压缩算法:
压缩:防止内存碎片产生,再次扫描,向一段移动存活的对象。
多了一个移动成本
标记清除压缩
- 先标记清除几次,如5次。在压缩。减少了 移动成本。
总结 5.分代收集算法
内存效率:复制算法>标记清除算法>标记压缩算法(时间复杂度)
内存整齐度:复制算法=标记压缩算法>标记清除算法
内存利用率:标记压缩算法=标记清除算法>复制算法
思考一个问题:难道没有最优算法吗?
答案:没有,没有最好的算法,只有最合适的算法----->
- GC∶分代收集算法
年轻代:
- 存活率低
- 复制算法!
老年代:
- 区域大:存活率
- 标记清除(内存碎片不是太多)+标记压缩混合实现
- 调优就是 调什么时候 清除和压缩
JMM
Java Memory Model
【JMM】(JavaMemory Model的缩写〉允许编译器和缓存以数据在处理器特定的缓存〈或寄存器)和主存之间移动的次序拥有重要的特权,除非程序员使用了volatile或synchronized明确请求了某些可见性的保证
作用:缓存一致性协议,用于定义数据读写的规则。
JMM定义了线程工作内存和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory)
解决共享对象可见性这个问题: volilate
- volatile声明或synchronized同步机制,
线程工作内存
每个线程都有自己的工作区域,是从主内存拷贝的
8种内存交互
java内存交互8种:
lock(锁定)——synchronized 和 ReentrantLock,锁的方式。主存变量加锁。
unlock(解锁)
read(读取):变量的值主存 传到 工作内存
load(载入):载入到 工作内存的变量副本。
use(使用):工作内存变量 传递给 执行引擎
assign(赋值):执行引擎 赋值 给工作内存的变量。
store(存储):工作内存变量 传输到 主内存中
write(写入):放入到主内存的变量中。
主存(主存变量) 工作内存(变量副本) 执行引擎
-
主存——》工作内存 读取
-
工作内存——》变量副本 载入 自己那块操作
-
工作内存——》执行引擎 使用
-
执行引擎——》工作内存 赋值
-
工作内存——》主存 存储
-
主存——》主存变量 写入。 自己那块操作
-
read:把一个变量的值从主内存传输到工作内存中
-
. load:在read之后执行,把read得到的值放入工作内存的变量副本中
-
. use:把工作内存中一个变量的值传递给执行引挚
-
. assign:把一个从执行引擎接收到的值赋给工作内存的变量.
-
store:把工作内存的一个变量的值传送到主内存中
-
write:在store 之后执行,把 store 得到的值放入主内存的变量中.
-
lock:作用于主内存的变量
-
. unlock
JMM规则
JMM对这八种指令的使用,制定了如下规则:
-
不许单独出现
-
改变后,要告知
- 没有 赋值的数据,不能同步到主存
- 变量必须内存中诞生
-
同一时间 只能一个 lock
- 没被lock,不能 unlock
- unlock之前,必须同步到主存。
-
·不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须
write -
不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存·
-
不允许一个线程将没有assign的数据从工作内存同步回主内存
-
一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作
-
.一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁·如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
-
如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
-
对一个变量进行unlock操作之前,必须把此变量同步回主内存
JMM对这八种操作规则和对volatile的一些特殊规则就能确定哪里操作是线程安全,哪些操作是线程不安全的了。但是这些规则实在复杂,很难在实践中直接分析。所以一般我们也不会通过上述规则进行分析。更多的时候,使用java的happen-before规则来进行分析。
volatile
volatile
英
/ˈvɒlətaɪl/
adj.
易变的,动荡不定的,反复无常的;(情绪)易变的,易怒的,突然发作的;(液体或固体)易挥发的,易气化的;(计算机内存)易失的
transient
adj.
转瞬即逝的,短暂的;暂住的,(工作)临时的
n.
暂住者,流动人口;(电流、电压、频率的)瞬变
//HashMap的
transient Node<K,V>[] table;
一、volatile关键字的作用
1、保证可见性;
2、防止指令重排;
3、但是不保证原子性;
二、可见性是什么?
在JMM(java memory model)java内存模型中,其他线程从主内存空间把值拷贝到自己的工作空间,线程修改之后的值会返回给主内存,主内存会通知其他线程,此为可见性。
三、指令重排
CPU为了执行效率会并发执行操作指令,volatile可以使指令一个一个的执行。
四、如何解决原子性问题
1、通过synchronized关键字。
2、通过使用AtomicXX,不加锁,采用CAS(compareAndSet)解决。其本质是使用UnSafe本地方法(CPU原语)。
- AtomicInteger
- new AtomicReference<Singleton_06>(); .compareAndSet(null, new Singleton_06());
3、使用LongAdder:最快(在线程多的情况下,使用分段锁)
transient
被transient修饰的变量不参与序列化和反序列化。
unsafe
unsafe可以帮我们直接去操作硬件资源,当然了是借助java的jit来进行的,官方不推荐使用,因为不安全,例如你使用unsafe创建一个超级大的数组,但是这个数组jvm是不管理的,只能你自己操作,容易oom,也不利于资源的回收.
//1.最简单的使用方式是基于反射获取Unsafe实例
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
JIT全程Java Intime Compiler,即Java即时编译器。