JVM概述与类的加载机制
JVM 内存模型
对象逃逸分析、JVM 内存分配和回收策略
垃圾回收算法详解、垃圾收集器全解
JVM 调优
垃圾回收算法详解、垃圾收集器全解
6 垃圾回收算法详解
6.1 引用计数算法
在对象中添加一个引用计数器,每当有一个地方 引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可 能再被使用的。在 java 领域一些主流虚拟机没有选用引用计数算法来进行内存管理。那么到底是因为什么呢?我们来往下看:
public class ReferenceCountingGC {
private Object instance;
public static void main(String[] args) {
ReferenceCountingGC obj1 = new ReferenceCountingGC();
ReferenceCountingGC obj2 = new ReferenceCountingGC();
obj1.instance = obj2;
obj2.instance = obj1;
obj1 = null;
obj2 = null;
System.gc();
}
}
如果上面代码这种情况,进行 GC 时使用引用计数算法,那么变量 obj1、obj2 的计数器值永远不为 0,就不会被收集。
6.2 可达性分析算法
这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到 GC Roots 间没有任何引用链相连,或者用图论的话来说就是从 GC Roots 到这个对象不可达时,则证明此对象是不可能再被使用的。
在 Java 技术体系里面,固定可作为 GC Roots 的对象包括以下几种:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。
- 在方法区中类静态属性引用的对象,譬如 Java 类的引用类型静态变量。
- 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
- 在本地方法栈中 JNI(即通常所说的 Native 方法)引用的对象。
- Java 虚拟机内部的引用,如基本数据类型对应的 Class 对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
- 所有被同步锁(synchronized 关键字)持有的对象。
- 反映 Java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等。
如下图所示,对象 object 5、object 6、object 7 虽然互有关联,但是它们到 GC Roots 是不可达的, 因此它们将会被判定为可回收的对象,这些对象就会被标记,进行后续处理。
6.3 判断对象可以被回收
6.3.1 引用类型
java 的引用类型一般分为 4 种:强引用、软引用、弱引用,虚引用
- 强引用:普通的变量引用
- 软引用:将对象用 SoftReference 软引用类型的的对象包裹,正常情况下不会被回收。但是,GC 之后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。软引用可用来实现对内存敏感度不高的高速缓存。
public static SoftReference<ShiroUser> user = new SoftReference<>(new ShiroUser());
- 弱引用:将对象用 WeakReference 弱类型的对象包裹,弱引用跟没引用差不多,GC 直接回收掉,很少用。
public static WeakReference<ShiroUser> user = new WeakReference<>(new ShiroUser());
- 虚引用:虚引用也称为幽灵引用或者幻影引用,是最弱的一种引用关系,几乎不用;。
6.3.2 finalize 方法最终判定
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候他们暂时处于缓刑阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。
1.第一次标记:对象在可达性分析后发现没有与 GC Roots 相连接的引用链,会被第一次标记。
2.进行一次筛选:筛选的条件是此对象是 否有必要执行 finalize()方法。‘
2.1 没必要直接被回收:假如对象没有覆盖 finalize()方法,或者 finalize()方法已经被虚拟机调用 过,那么虚拟机将这两种情况都视为“没有必要执行”。
2.2 放在 F-Queue 队列中:假如这个对象被判定为有必要执行 finalize()方法,则会被放在该队列中。
3.第二次标记:稍后收集器将对 F-Queue 中的对象进行第二次小规模的标记。
3.1 如果对象在 finalize()中成功拯救自己(把自己与引用链上任何一个对象建立关联),在第二次标记时就会被移出即将被回收的集合。
3.2 如果此时还么与引用链上的对象关联,基本上就要被回收了。
如下代码:
//VM: -Xms10M -Xmx10M -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
// -XX:HeapDumpPath=D:\jvm.dump
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
int i = 0;
int j = 0;
while (i<10001){
list.add(new User(i++, Long.toString(System.currentTimeMillis()))); // 关联了 list,不会被回收
new User(j--, Long.toString(System.currentTimeMillis())); // 会被回收,怎么拯救呢
}
}
public class User {
private Integer id;
private String name;
public User(){}
public User(Integer id, String name){
super();
this.id = id;
this.name = name;
}
@Override
protected void finalize() throws Throwable {
OOMTest.list1.add(this);
System.out.println("关闭资源,user" + id + "即将被回收");
}
public class User {
private Integer id;
private String name;
public User(){}
public User(Integer id, String name){
super();
this.id = id;
this.name = name;
}
@Override
protected void finalize() throws Throwable {
OOMTest.list1.add(this);
System.out.println("关闭资源,user" + id + "即将被回收");
}
User 类如下:
public class User {
private Integer id;
private String name;
public User(){}
public User(Integer id, String name){
super();
this.id = id;
this.name = name;
}
// get,set 方法略
我们在User类里面重写下finalize()方法并且把当前对象与调用链相关联,就可以让该对象“惨遭一死”了。修改后代码如下:
public class OOMTest {
public static List<Object> list1 = new ArrayList<>();
//VM: -Xms10M -Xmx10M -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
// -XX:HeapDumpPath=D:\jvm.dump
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
int i = 0;
int j = 0;
while (i<10001){
list.add(new User(i++, Long.toString(System.currentTimeMillis()))); // 关联了 list,不会被回收
new User(j--, Long.toString(System.currentTimeMillis())); // 会被回收,怎么拯救呢
}
}
}
User 类如下:
public class User {
private Integer id;
private String name;
public User(){}
public User(Integer id, String name){
super();
this.id = id;
this.name = name;
}
// 重写 finalize()方法
@Override
protected void finalize() throws Throwable {
OOMTest.list1.add(this); // 与引用链关联
System.out.println("关闭资源,user" + id + "即将被回收");
}
// get,set 方法略
6.3.3 判断类是否可以被回收
类需要同时满足 3 个条件才能算是“无用的类”:
- 该类所有的实例都已经被回收
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过放射访问该类的方法。
6.4 垃圾收集算法
6.4.1 标记-清除算法
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。
回收过程示意图:
问题:
效率问题
空间问题(标记清除后会产生大量不连续的碎片)
6.4.2 复制算法
复制算法是将内存分为大小相同的两块,每次只用其中一块,当这一块用完了,就将还存活的对象复制到另外一块上面,让后把已经使用过的内存空间一次性清理掉。
效率低
不会产生不连续的空间
6.4.3 标记-整理算法
根据老年代的特点,有人提出了另外一种标记-整理算法,过程与标记清除算法一致,不过不是直接对可回收对象进行清理,而是让所有存活对象都想一端移动,让后清理掉边界以外的内存。
6.4.4 分代收集算法
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
- 比如在新生代中,每次收集都会有大量对象(近 99%)死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集;
- 而老年代的对象存活几率是比较高的,而且没有额外的空间为他们进行分配担保,所以我们必须选择“标记-清除”算法或“标记-整理”算法进行垃圾收集。
7 垃圾收集器全解
7.1 垃圾收集器概述
如果说收集算法是内存回收的方法论,那垃圾收集器就是内存回收的实践者。
上图展示了七种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配 使用,图中收集器所处的区域,则表示它是属于新生代收集器抑或是老年代收集器
7.2 Serial 收集器
VM:-XX:+UseSerialGC -XX:+UseSerialOldGC
serial(串行)收集器是一个单线程收集器,单线程一方面意味着只会使用一个 CPU 或者一条线程完成垃圾收集工作,另一方面也意味着它进行垃圾收集时必须暂停其他线程的所有工作(“Stop The Word"),直到它收集结束为止。
下图示意了 Serial/Serial Old 收 集器的运行过程。
耽误其他线程任务
如果服务器是多核,该收集器也是单线程去处理,浪费资源。
7.3 ParNew 收集器
VM: -XX:+UseParNewGC
ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为和 Serial 收集器完全一样。默认的收集线程数量跟 CPU 核数相同,当然也可以用参数(
-XX:ParallelGCThreads)指定垃圾收集线程数量,一般不推荐修改。
收集器的工作过程如下图:
7.4 Scavenge 收集器
Parallerl Scavenge收集器,VM参数如下:
-XX:+UseParallelGC(年轻代)
-XX:+UseParallelOldGC(老年代)
- Paraller Scavenge 收集器类似于 ParNew 收集器,是 server 模式(内存大于 2G,2 个 CPU)下的默认收集器。
- Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。吞吐量=运行用户代码时间 / (运行用户代码时间+垃圾收集时间)
下图为该收集器的运行过程示意图:
7.5 CMS 垃圾收集器
7.5.1 运行过程
VM 参数:-XX:+UseMarkSweepGC(老年代)
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。他非常符合在注重用户体验的应用上使用,它是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次出现了让垃圾收集线程与用户线程同时(基本上)工作。
从名字(包含“Mark Sweep”)上就可以看出 CMS 收集器是基于标记-清除算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括:
1)初始标记(CMS initial mark)
2)并发标记(CMS concurrent mark)
3)重新标记(CMS remark)
4)并发清除(CMS concurrent sweep)
Concurrent Mark Sweep 收集器运行示意图:
7.5.2 优缺点
优缺点如下:
优点:并发收集,低停顿。
缺点:
- 对 CPU 资源敏感(会和服务抢资源)
- 无法处理浮动垃圾(在并发清理阶段又产生垃圾,这种浮动垃圾只能等下一次 GC 再清理了)
- 它使用的回收算法“标记-清除”算法会导致收集结束时,会有大量空间碎片的产生,当然通过参数:-XX:+UseCMSCompactAtFullConllection 可以让 JVM 在执行完“标记-清除”后再整理。
- 执行过程中不确定性,会存在上一次的垃圾回收还没执行完,然后垃圾回收又被出发的情况,特别是在并发标记和并发清理阶段出现,一边回收,系统一边运行,也许还没回收完就再次触发 Full GC,也就是“concurrent mode failure",此时会进入 stop the word,用 Serial Old 垃圾回收器来回收。
7.5.3 相关参数
-XX:+UseConcMarkSweepGC ——启用CMS
-XX:ConcGCThreads ——并发的GC线程数
-XX:+UseCMSCompactAtFullCollection ——Full GC后做压缩整理(减少碎片)
-XX:CMSFullGCsBeforeCompaction ——多少次GC之后压缩一次,默认是0,代表每次GC后都会压缩一次
-XX:CMSInitiatingOccupancyFraction ——当老年代使用达到该比例时会触发Full GC(默认是92,这是百分比)
-XX:+UseCMSInitiatingOccupancyOnly ——只使用设定的回收阈值(
-XX:CMSInitiatingOccupancyFraction 设定的值),如果不指定,JVM 会在第一次使用设定值,后续则会自动调整
-XX:+CMSSsacengeBeforeRemark ——在CMS GC前启动一次minor GC,目的在于减少老年代对年轻代的引用,降低CMS GC的标记阶段时的开销,一般CMS的耗时80%都在标记阶段。
7.6 G1 垃圾收集器
VM 参数:-XX:+UseG1GC
G1(Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大量内存的机器,以及高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。
7.6.1 G1 垃圾收集器分区
内存各分代不再连续,把整个堆内存分为 2048 块,每块称为一个区(region)。每个区的大小为堆内存大小/2048。内存分布如下图:
其中:Humongous 是存放大对象的,如果一个对象过大,可以用多个连续的 Humongous 去存放。
7.6.2 G1 收集器运行过程
G1 收集器一次 GC 的运行过程大致分为以下几个步骤:
- 初始标记(inital mark, STW):暂停所有的其他线程,并记录下 GC Roots 直接引用的对象,速度很快。
- 并发标记(Concurrent Marking):同 CMS 的并发标记
- 最终标记(Remark,STW):同 CMS 的重新标记
- 筛选回收(Clearup, STW):筛选回收阶段首先对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间(可以用 jvm 参数:-XX:MaxGCPauseMillis 指定,默认 200ms)来制定回收计划。
7.6.3 G1 垃圾收集器的分类
YoungGC
YoungGC 并不是说现有的 Eden 区放满了马上就会触发,G1 会计算下现在 Eden 区回收大概需要多久时间,如果回收时间远远小于参数:-XX:+MaxGCPauseMillis 设定的值,那么增加年轻代的 region,继续给新对象存放,不会马上做 YoungGC,直到下一次 Eden 区放满,G1 计算回收时间接近参数-XX:+MaxGCPauseMillis 设定的值,那么就会触发 YoungGC.
MixedGC
不是 FullGC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发,回收所有的 Young 和部分 Old(根据期望的 GC 停顿时间确定 Old 区垃圾收集的优先顺序)以及大对象区。
FullGC
停止系统程序,然后采用单线程进行标记,清理和压缩整理,好空闲出来一批 region 来供下一次 MixedGC 使用,这个过程非常耗时。