垃圾回收器
1、GC 分类与性能指标
1.1、垃圾回收器概述与分类
垃圾回收器概述
- 垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商、不同版本的JVM来实现。
- 由于JDK的版本处于高速迭代过程中,因此Java发展至今已经衍生了众多的GC版本。
- 从不同角度分析垃圾收集器,可以将GC分为不同的类型。
Java不同版本新特性
- 语法层面:Lambda表达式、switch、自动拆箱装箱、enum
- API层面:Stream API、新的日期时间、Optional、String、集合框架
- 底层优化:JVM优化、GC的变化、元空间、静态域、字符串常量池位置变化
垃圾回收器 分类
按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器。
1.2、评估 GC 的性能指标
-
吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间)
-
垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例。
-
暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
-
收集频率:相对于应用程序的执行,收集操作发生的频率。
-
内存占用:Java堆区所占的内存大小。
-
快速:一个对象从诞生到被回收所经历的时间。
吞吐量、暂停时间、内存占用这三者共同构成一个“不可能三角”。三者总体的表现会随着技术进步而越来越好。一款优秀的收集器通常最多同时满足其中的两项。
这三项里,暂停时间的重要性日益凸显。因为随着硬件发展,内存占用多些越来越能容忍,硬件性能的提升也有助于降低收集器运行时对应用程序的影响,即提高了吞吐量。而内存的扩大,对延迟反而带来负面效果。
简单来说,因为内存不值钱了,主要抓住两点:
- 吞吐量
- 暂停时间
评估 GC 的性能指标:吞吐量(throughput)
https://www.bilibili.com/video/BV1PJ411n7xZ?p=172
评估 GC 的性能指标:暂停时间(pause time)
对比
吞吐量:一顿吃的多,跑的多;
低延迟:吃得少,吃得勤,跑的少;
2、不同的垃圾回收器概述
https://www.bilibili.com/video/BV1PJ411n7xZ?p=173&spm_id_from=pageDriver
- 垃圾收集机制是Java的招牌能力,极大地提高了开发效率。这当然也是面试的热点。
- GC垃圾收集器是和JVM一脉相承的,它是和JVM进行搭配使用,在不同的使用场景对应的收集器也是有区别
- 那么,Java常见的垃圾收集器有哪些?
2.1、垃圾收集器发展史
7种经典的垃圾收集器
2.2、回收器与垃圾分代之间的关系
两个收集器间有连线,表明它们可以搭配使用:
-
Serial/Serial old
-
Serial/CMS
-
ParNew/Serial Old
-
ParNew/CMS
-
Parallel Scavenge/Serial Old
-
Parallel Scavenge
-
Parallel Old、G1;
其中Serial Old作为CMS出现"Concurrent Mode Failure"失败的后备预案。
(红色虚线)由于维护和兼容性测试的成本,在JDK 8时将Serial+CMS、ParNew+Serial Old这两个组合声明为废弃(JEP173),并在JDK9中完全取消了这些组合的支持(JEP214),即:移除。
(绿色虚线)JDK14中:弃用Parallel Scavenge和Serial Old GC组合(JEP366)
(青色虚线)JDK14中:删除CMS垃圾回收器(JEP363)
2.3、查看默认垃圾收集器
https://www.bilibili.com/video/BV1PJ411n7xZ?p=175
设置 -XX:+PrintCommandLineFlags 查看
- 在 JDK 8 下,设置 JVM 参数
-XX:+PrintCommandLineFlags
程序打印输出:-XX:+UseParallelGC 表示使用使用 ParallelGC ,ParallelGC 默认和 Parallel Old 绑定使用
-XX:InitialHeapSize=266620736
-XX:MaxHeapSize=4265931776
-XX:+PrintCommandLineFlags
-XX:+UseCompressedClassPointers
-XX:+UseCompressedOops
-XX:-UseLargePagesIndividualAllocation
-XX:+UseParallelGC
通过命令行指令查看
命令行命令
jps
jinfo -flag UseParallelGC 进程id
jinfo -flag UseParallelOldGC 进程id
JDK 8 中默认使用 ParallelGC 和 ParallelOldGC 的组合
3、Serial 回收器:串行回收器
4、ParNew 回收器:并行回收
- 只能处理新生代
- 复制算法
- 并行回收
- stop-the world
并行 or 串行
ParNew 与 Serial 比较
使用
5、Parallel 回收器:吞吐量优先
Parallel 和 ParNew
-
Parallel Scavenge收集器 和 ParNew收集器 同样也采用了复制算法、并行回收 和 "Stop the World"机制 。
-
那么Parallel收集器的出现是否多此一举?
(1)和ParNew收集器不同,Parallel Scavenge收集器的目标则是达到一个 可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾收集器
(2)自适应调节策略也是Parallel Scavenge与ParNew一个重要区别。
自适应调节策略:jvm运行过程中性能监控,动态调整内存分配情况,以适合以吞吐量 优先 还是低延迟
高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。因此,常见在服务器环境中使用。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序。
Parallel Old 配合使用
Parallel收集器在JDK1.6时提供了用于执行老年代垃圾收集的Parallel Old收集器,用来代替老年代的Serial Old收集器(如果没有Parallel Old ,老年代只能用Serial Old 串行回收,新生代都有Parallel了,说明服务器性能不错,没必要使用低效率的Serial Old)。
Parallel Old收集器采用了标记-压缩算法,但同样也是基于并行回收和 "Stop-the-World"机制。
Parallel 回收器参数设置
第三个是cpu核心数
默认开启;
6、CMS 回收器:低延迟
https://www.bilibili.com/video/BV1PJ411n7xZ?p=182&spm_id_from=pageDriver
工作原理
CMS 特点和弊端
https://www.bilibili.com/video/BV1PJ411n7xZ?p=183&spm_id_from=pageDriver
-
尽管CMS收集器采用的是并发回收(非独占式),但是在其初始化标记和再次标记这两个阶段中仍然需要执行“Stop-the-World”机制暂停程序中的工作线程,不过暂停时间并不会太长,因此可以说明目前所有的垃圾收集器都做不到完全不需要“Stop-the-World”,只是尽可能地缩短暂停时间。
-
由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的。另外,由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。
因此,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。
要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
CMS收集器的垃圾收集算法采用的是标记清除算法,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块,不可避免地将会产生一些内存碎片。那么CMS在为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配。
总结
6.5、CMS 参数配置
CMS 小结
https://www.bilibili.com/video/BV1PJ411n7xZ?p=185&spm_id_from=pageDriver
7. G1 回收器:区域化分代式
7.1 认识G1
全功能收集器: 延迟可控范围内尽可能高的吞吐量
为什么叫G1?
7.2 G1 优劣势
https://www.bilibili.com/video/BV1PJ411n7xZ?p=187&spm_id_from=pageDriver
(1)兼具并行与并发
- 并行:G1在回收期间,可以有多个GC线程同时工作,有效利用多核的优势;不过此期间用户线程会STW
- 并发:G1线程拥有和用户线程交替执行的能力,G1回收期间的部分工作可以和用户线程同时执行;因此,不会在整个回收阶段发生完全阻塞应用程序的情况;
(2)分代收集
-
G1 也会区分新生代和老年代; 新生代仍然有Eden和Survivor区;
-
将对空间区分成若干个区域(region),这些区域被逻辑归属于年轻代和老年代; 从堆结构来看,G1不要求Eden区、年轻代、老年代是连续的,也不坚持固定大小和数量;
-
G1 一个垃圾收集器同时兼顾新生代和老年代;其他垃圾收集器要么工作在年轻代,要么工作在老年代
(2)空间整合能力
(4)可预测的停顿时间模型
- 有了region,相当于对分代更细分!回收区域更小
- 回收哪些区域?=> region 垃圾价值大小排序
- 实时:在指定的时间范围内必须GC完毕;软实时:允许有少量误差
缺点
7.3 G1 参数设置
https://www.bilibili.com/video/BV1PJ411n7xZ?p=188&spm_id_from=pageDriver
7.4 G1 使用场景
https://www.bilibili.com/video/BV1PJ411n7xZ?p=189&spm_id_from=pageDriver
7.5 Region介绍
7.6 G1 垃圾回收流程
https://www.bilibili.com/video/BV1PJ411n7xZ?p=191
G1 回收器垃圾回收过程:Remembered Set(记忆集)
https://www.bilibili.com/video/BV1PJ411n7xZ?p=193
一个对象被不同区域引用的问题
问题描述:
一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,判断对象存活时,是否需要扫描整个Java堆才能保证准确?
- 在其他的分代收集器,也存在这样的问题(而G1更突出,因为G1主要针对大堆)
- 回收新生代也不得不同时扫描老年代?这样的话会降低Minor GC的效率
解决方法:
-
无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描;
-
每个Region都有一个对应的Remembered Set
-
每次往Region中写入一个Reference类型数据(对象)时,都会产生一个Write Barrier暂时中断操作,然后检查将要写入的对象是否和引用该对象的数据在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象);
-
- 如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中;
-
- 当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set;就可以保证不进行全局扫描,也不会有遗漏。(RS中的就也算在GC Roots可达范围内)
几句话总结
- 在回收 Region 时,为了不进行全堆的扫描,引入了 Remembered Set
- Remembered Set 记录了当前 Region 中的对象被哪个对象引用了
- 这样在进行 Region 复制时,就不要扫描整个堆,只需要去 Remembered Set 里面找到引用了当前 Region 的对象
- Region 复制完毕后,修改 Remembered Set 中对象的引用即可
G1 年轻代 GC
回收过程:
第一阶段,扫描根(找GC Roots)
- 根是指static变量指向的对象,正在执行的方法调用链条上的局部变量等。
- 根引用连同RSet记录的外部引用作为扫描存活对象的入口。
第二阶段,更新RSet(保证RSet准确)
- 处理dirty card queue(见备注)中的card,更新RSet。
- 此阶段完成后,RSet可以准确的反映老年代对所在的内存分段中对象的引用。
第三阶段,处理RSet
- 识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象。
第四阶段,复制对象。
- 此阶段,对象树被遍历,Eden区内存段中存活的对象会被复制到Survivor区中空的内存分段,Survivor区内存段中存活的对象
- 如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到Old区中空的内存分段。
如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间。
第五阶段,处理引用
- 处理Soft,Weak,Phantom,Final,JNI Weak 等引用。最终Eden空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。
备注:
- 对于应用程序的引用赋值语句 oldObject.field=new Object(),JVM会在之前和之后执行特殊的操作以在dirty card queue中入队一个保存了对象引用信息的card。
- 在年轻代回收的时候,G1会对Dirty Card Queue中所有的card进行处理,以更新RSet,保证RSet实时准确的反映引用关系。
那为什么不在引用赋值语句处直接更新RSet呢?
- 这里设计在Young GC的时候更新RSet,这是为了性能的需要,RSet的处理需要线程同步,开销会很大,使用队列性能会好很多。
G1 并发标记过程
G1 混合回收过程
混合回收的细节
G1 回收可选的过程四:Full GC
G1 回收器的补充
https://www.bilibili.com/video/BV1PJ411n7xZ?p=194
- 从Oracle官方透露出来的信息可获知,回收阶段(Evacuation)其实本也有想过设计成与用户程序一起并发执行,但这件事情做起来比较复杂,考虑到G1只是回一部分Region,停顿时间是用户可控制的,所以并不迫切去实现,而选择把这个特性放到了G1之后出现的低延迟垃圾收集器(即ZGC)中。
- 另外,还考虑到G1不是仅仅面向低延迟,停顿用户线程能够最大幅度提高垃圾收集效率,为了保证吞吐量所以才选择了完全暂停用户线程的实现方案。
G1 回收器的优化建议
8.垃圾回收器总结
截止JDK1.8,一共有7款不同的垃圾收集器。每一款的垃圾收集器都有不同的特点,在具体使用的时候,需要根据具体的情况选用不同的垃圾收集器。
GC发展阶段: Seria l=> Parallel(并行)=> CMS(并发)=> G1 => ZGC
不同厂商、不同版本的虚拟机实现差距比较大。HotSpot虚拟机在JDK7/8后所有收集器及组合如下图(更新至JDK14)
怎么选择垃圾回收器
关于面试
9. GC 日志分析(☆)
https://www.bilibili.com/video/BV1PJ411n7xZ?p=196&spm_id_from=pageDriver
GC 日志参数设置
GC 日志补充说明
YounGC
Full GC
GC日志实战
/**
* 在jdk7 和 jdk8中分别执行
* -verbose:gc -Xms20M -Xmx20M 堆20m
* -Xmn10M 新生代10m
* -XX:+PrintGCDetails
* -XX:SurvivorRatio=8 新生代8:1:1
* -XX:+UseSerialGC
*
* @author shkstart shkstart@126.com
* @create 2020 0:12
*/
public class GCLogTest1 {
private static final int _1MB = 1024 * 1024;
public static void testAllocation() {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
allocation4 = new byte[4 * _1MB];
}
public static void main(String[] agrs) {
testAllocation();
}
}
JDK7 中的情况
- 首先我们会将3个2M的数组存放到Eden区,然后后面4M的数组来了后,将无法存储,因为Eden区只剩下2M的剩余空间了,那么将会进行一次Young GC操作
- YoungGC 会将原来Eden区的内容,存放到Survivor区,但是Survivor区也存放不下,那么就会直接晋级存入Old 区
JDK8 中的情况
与 JDK7 不同的是,JDK8 直接判定 4M 的数组为大对象,直接怼到老年区去了
常用日志分析工具
https://www.bilibili.com/video/BV1PJ411n7xZ?p=199&spm_id_from=pageDriver
10 垃圾回收器的新发展
https://www.bilibili.com/video/BV1PJ411n7xZ?p=200
回首过去
展望未来
Shenandoah GC
革命性的 ZGC
JDK14 新特定:ZGC