本篇文章是对:深入理解Java虚拟机系列文章 的一份精炼总结。
针对JDK8
来说,JVM
内存模型包括:
Native
修饰的函数。Java
对象实例。如果按照线程是否私有来区分:
其他特点:
OOM
的区域。Java
虚拟机栈:线程私有、存放了基本数据类型的变量。GC
堆):虚拟机内存中最大的一块、线程共享、在虚拟机启动的时候创建。存储了静态变量、常量等数据。Serial、ParNew
使用时会用到)CMS
收集器使用时会用到)Java
对象的内存存储布局如下,分为三块区域:
对象头又包括:
MarkWord
:存储自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏相关时间戳等真正保存实例数据的地方。
填充Java
对象,让它为8的整数倍。
有一个地方在引用,计数器+1,引用失效时,计数器-1。任何时刻计数器为0的对象就是不可能在被使用的。:
算法的主要思路:
可以作为Root
对象包括:
native
方法)引用的变量。一共4种,强度从大到小排序:
一般是new
出来的对象。只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
软引用是用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列入回收范围内进行第二次回收。
被弱引用关联的对象只能生存到下一次GC发生之前,GC工作的时候,无论当前内存是否足够,都会回收到只被弱引用关联的对象。
例如ThreadLocal
类中底层ThreadLocalMap
的Key
就是一个弱引用。
虚引用是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就就是能在这个对象被GC回收的时候得到一个系统通知。
要真正宣告一个对象死亡的话,至少要经历两次标记过程。(GC回收器的算法都会经历2次标记)
如何摆脱死亡的命运呢?大致流程如下:
GC Root
相连的引用链,那么他会被第一次标记并且进行筛选.finalize()
方法。finalize()
方法被覆盖过(重写)且没被执行过,那么这个对象会被判为有必要执行finalize
方法的。F-Queue
的队列之中,并在稍后由一个低优先级的Finalizer
线程去触发这个finalize()
方法。finalize()
方法中成功的让此对象重新与引用链上的任何一个对象关联(即可达),那么在二次标记的时候,就会把这个对象移出“即将回收”的集合。**相反,如果执行后,这个对象还是不可达的,那么他就会被回收。总结:重写一个对象的finalize
函数,让这个对象做到可达,即可能让这个对象逃脱一次被GC
的命运。
算法划分为两个阶段。
缺点:
算法流程如下:
优缺点:
算法流程如下:
优点:
GC
算法。STW(Stop The World)
:枚举根节点时必须停顿,避免引用关系发生变化。使用OopMap
来实现快速定位GC Roots
的枚举。GC
的时候,程序一定到达了某个安全点(Safepoint
)或者安全区域。新生代GC
:
Serial
(单线程)ParNew
(多线程)Parallel Scavenge
(多线程,目标:达到一个可控制的吞吐量。支持自适应调节策略)老年代GC
:
Serial Old
(单线程、标记整理)Parallel Old
(参考Parallel Scavenge
,只是服务的范围不一样、标记整理)CMS
(目标:获取最短回收停顿时间,并发收集,标记清除)独立的GC
:
G1
(标记整理、复制算法、并行与并发、分代、可预测停顿)加载阶段需要做三件事情:
java.lang.Class
对象,作为方法区这个类的各种数据的访问接口。确保Class
文件的字节流中包含的信息符合当前虚拟机的要求,并不会危害虚拟机自身的安全。验证的内容包括:
Class
文件格式的规范,并能被当前版本的虚拟机处理。Java
语言规范。准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,仅仅针对被static
修饰的变量。
但是值得注意的是!!!如以下代码:
public static int value = 123;
在准备阶段,赋值初始值的时候,value
的值是0,而不是123。但是如果代码是:
public static final int value = 123;
那这个时候value
在准备阶段会赋值为123。static final
修饰的字段在javac
编译时生成constantValue
属性,在类加载的准备阶段直接把constantValue
的值赋给该字段。
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
在初始化阶段,会根据我们自己制定的Java
代码去初始化类变量和其他资源。换句话说,初始化阶段是执行类构造器< clinit >()
方法的过程。
< clinit >()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{})合并而成。
说白了就是:static语句块和声明的static变量。
初始化阶段有以下几个重点知识:
< clinit >()
方法执行之前,父类的< clinit >()
方法已经执行完毕。即父类中定义的静态语句块要优先于子类的变量赋值操作。如果能通过一个类的全限定名称来获取描述该类的二进制字节流,那这样的角色称作为类加载器。
若两个类来源于同一个Class
文件,被同一个虚拟机加载,只要加载他们的类加载器不同,这两个类就必定不相等。
类加载器的种类如下:
Bootstrap ClassLoader
):由C++
语言实现,是JVM
自身的一部分。Extension ClassLoader
):负责加载< JAVA_HOME>\lib\ext
目录中的或者被java.ext.dirs
系统变量所制定的路径中的所有类库。(开发者可以直接使用)Application ClassLoader
):负责加载用户类路径(ClassPath
)上所制定的类库。双亲委派模型的工作流程:
什么情况下要打破?
class
文件。JDBC
中的Driver
接口的实现是由不同的数据库服务商来提供,有Mysql、Oracle、KingbaseES
等数据库。它是由启动类加载器来实现加载(顶层父类),但是具体实现却在子类,因此需要用户程序类加载器加载。Driver
的具体实现volatile
关键字是Java
虚拟机提供的最轻量级的同步机制。当一个变量定义为volatile
后,它具备两种特性(不保证原子性):
volatile
修饰的变量的值,那么该新值对于其他线程来说立即可见。问题1:什么是指令重排?
指令重排是指JVM
在编译Java
代码的时候,或者CPU
在执行JVM
字节码的时候,对现有的指令顺序进行重新排序。
问题2:指令重排的目的是什么?
指令重排的目的是为了在不改变程序执行结果的前提下,优化程序的运行效率(指的是不改变单线程下的程序执行结果)。
加了volatile
修饰的变量,处理器会多出一个带有Lock
的汇编指令。
而Lock
前缀指令主要做了两件事情:
第一个:将当前处理器缓存行的数据回写到内存当中。
第二个:这个写回内存的操作会使其他CPU
缓存了该内存地址的数据无效。(内存屏障的功能之一)
换句话说,lock
指令加了一个内存屏障,禁止在内存屏障前后的指令执行重排序优化。
背景:
instance = new Singleton();
并不是一个原子操作,会被编译成三条指令(按顺序)。1.给instance
分配内存。2.初始化其构造器。3.将instance
对象指向分配的内存空间。volatile
,可能会发生重排序,导致可能的执行顺序是132,从而导致某些线程访问到未初始化的变量。volatile
来禁止重排序,通过加入内存屏障的方式来保证执行的顺序为123。双重检索:
public class Singleton {
private volatile static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
public static void main(String[] args) {
Singleton.getInstance();
}
}
原子性:Java内存模型直接保证的原子性变量操作包括:read、load、assign、use、store、write
。
可见性:可见性指一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
有序性:Java程序中天然的有序性可以概括为:如果在本线程内观察,所有的操作都是有序的。Java提供了volatile和synchronized两个关键字来保证线程之间操作的有序性。
volatile
不会进行加锁操作(它只是一种稍弱的同步机制),而Synchronized
会进行加锁。volatile
变量作用类似于同步变量的读写操作,从内存可见性角度来看:1.写入volatile
变量——>退出同步代码块。2.读取volatile
变量——>进入同步代码块。volatile
不如Synchronized
安全(前者无锁,后者有)volatile
不能同时保证内存可见性和原子性,但是Synchronized
可以。如果持有锁的线程能够在很短的时间内释放资源,那么那些正在等待的线程就不用做内核态和用户态的转换而进入堵塞、挂起状态。只要等待一小段时间,就能在其他线程释放资源的瞬间可以立即获得锁。
锁消除是指虚拟机在编译器运行时,对一些代码要求同步,但是被检测到实际上不存在共享数据的竞争,那么对于这一类锁,会进行消除。
例如StringBuffer.append
函数,就由synchronized
关键字修饰。
public String method(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
// 其中append源码:
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
这种时候我们可以添加参数进行锁的消除。
-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
将两个同步代码块合并成一个,以降低多次锁请求、同步、释放带来的系统性能消耗。
偏向锁是指一段同步代码块一直被一个线程访问,那么该线程会自动获取锁,降低获取锁的代价。 其目标是在只有一个线程执行同步代码块的时候能够提高性能。