垃圾回收器
- 1、GC分类与性能指标
-
- 1.1、GC分类
- 1.2、评估GC的性能指标
- 2、不同的垃圾回收器概述
-
- 2.1、垃圾回收器的发展
- 2.2、7款经典的垃圾收集器及其与垃圾分代的关系
- 2.3、垃圾收集器的组合关系
- 2.4、查看默认的垃圾回收器
- 3、Serial、SerialOld 回收器:串行回收(了解)
- 4、ParNew回收器:并行回收(了解)
- 5、Parallel、ParallelOld:吞吐量优先(Java8 默认)
- 6、CMS回收器:低延迟
-
- 6.1、CMS概述
- 6.2、CMS过程
- 6.3、CMS优缺点
- 6.4、CMS参数设置
- 7、重点:面试相关
1、GC分类与性能指标
1.1、GC分类
-
按线程数分(垃圾回收线程数),可以分为串行垃圾回收器和并行垃圾回收器。
串行回收默认被应用在客户端的Client模式下的JVM中
-
按照工作模式分,可以分为独占式垃圾回收器(Stop the World)和并发式垃圾回收器。
-
按碎片处理方式分,可分为压缩式垃圾回收器(指针碰撞)和非压缩式垃圾回收器(空闲列表)。
-
按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器。
1.2、评估GC的性能指标
-
吞吐量 = 运行用户代码的时间 / ( 运行用户代码的时间 + 内存回收的时间)
吞吐量优先,意味着在单位时间内,STW的时间最短。必然需要降低内存回收的执行频率。 -
垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例。
-
暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
暂停时间优先,意味着尽可能让单次STW的时间最短。只能频繁的执行内存回收 -
收集频率:相对于应用程序的执行,收集操作发生的频率。
-
内存占用:Java堆区所占的内存大小。
-
快速:一个对象从诞生到被回收所经历的时间。
吞吐量、暂停时间、内存占用这三者共同构成一个“不可能三角”,主要抓住两点:吞吐量、暂停时间
现在JVM调优标准:在最大吞吐量优先的情况下,降低停顿时间(在可控的暂停时间内,尽量增加吞吐量)。
2、不同的垃圾回收器概述
2.1、垃圾回收器的发展
有了虚拟机,就一定需要收集垃圾的机制,这就是Garbage Collection,对应的产品我们称为Garbage Collector。
- 1999年随JDK1.3.1一起来的是串行方式的Serial GC,它是第一款GC。ParNew垃圾收集器是Serial收集器的多线程版本。
- 2002年2月26日,Parallel GC和Concurrent Mark Sweep GC跟随JDK1.4.2一起发布。
- Parallel GC在JDK6之后成为HotSpot默认GC。
- 2012年,在JDK1.7u4版本中,G1可用。
- 2017年,JDK9中G1变成默认的垃圾收集器,以替代CMS。
- 2018年3月,JDK10中G1垃圾回收器的并行完整垃圾回收,实现并行性来改善最坏情况下的延迟。
- 2018年9月,JDK11发布。引入Epsilon 垃圾回收器,又被称为 "No-Op(无操作)“ 回收器。
同时,引入ZGC:可伸缩的低延迟垃圾回收器(Experimental) - 2019年3月,JDK12发布。增强G1,自动返回未用堆内存给操作系统。同时引入Shenandoah GC:低停顿时间的GC(Experimental)。
- 2019年9月,JDK13发布。增强ZGC,自动返回未用堆内存给操作系统。
- 2020年3月,JDK14发布。删除CMS垃圾回收器。扩展ZGC在macOS和Windows上的应用
2.2、7款经典的垃圾收集器及其与垃圾分代的关系
- 串行回收器:Serial、Serial old
- 并行回收器:ParNew、Parallel Scavenge、Parallel old
- 并发回收器:CMS、G1
相关资料:https://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf
- 新生代收集器:Serial、ParNew、Parallel Scavenge
- 老年代收集器:Serial old、Parallel old、CMS
- 整堆收集器:G1
2.3、垃圾收集器的组合关系
- 两个收集器间有连线,表明可以搭配使用。虚线表示已废除方案。
- Serial Old 作为 CMS 出现“Concurrent Mode Failure” 失败的后备方案。
- (红色虚线)由于维护和兼容性测试的成本,在 JDK8 时将Serial+CMS、ParNew+Serial Old这两个组合声明为Deprecated(JEP173),并在 JDK9 中完全取消了这些组合的支持(JEP214),即:移除。
- (绿色虚线)JDK14 中:弃用ParallelScavenga 和 SerialOld GC 组合(JEP 366)
- (青色虚线)JDK14 中:删除CMS垃圾回收器(JEP 363)
- GC发展阶段:Serial -> Parallel(并行)->CMS(并发)->G1 -> ZGC
2.4、查看默认的垃圾回收器
/*** -XX:+PrintCommandLineFlags 查看程序使用的默认JVM参数* -XX:+UseParNewGC:标明新生代使用ParNew GC * * -XX:+UseSerialGC:表明新生代使用Serial GC ,同时老年代使用Serial Old GC* -XX:+UseParallelGC:表明新生代使用Parallel GC * -XX:+UseParallelOldGC : 表明老年代使用 Parallel Old GC* 说明:二者可以相互激活* -XX:+UseConcMarkSweepGC:表明老年代使用CMS GC。同时,年轻代会触发对ParNew 的使用*/
public class GCUseTest {
public static void main(String[] args) {
ArrayList<byte[]> list = new ArrayList<>();while(true){
byte[] arr = new byte[100];list.add(arr);try {
Thread.sleep(10);} catch (InterruptedException e) {
e.printStackTrace();}}}
}
输出
-XX:InitialHeapSize=264155648 -XX:MaxHeapSize=4226490368 -XX:+PrintCommandLineFlags
-XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
控制台查看垃圾回收器
3、Serial、SerialOld 回收器:串行回收(了解)
- Serial收集器采用复制算法、串行回收和“Stop-the-World”机制的方式执行内存回收。
- Serial Old 收集器同样也采用了串行回收 和"Stop the World"机制,只不过内存回收算法使用的是标记—压缩算法。
- 单线程回收:使用一个cpu或一条线程去完成垃圾收集工作,必须暂停其他所有的工作线程,直至它收集结束(Stop The World)
- 参数 -XX:+UseSerialGC 参数可以指定年轻代和老年代都使用串行收集器
等价于新生代用Serial GC,且老年代用Serial Old GC。
4、ParNew回收器:并行回收(了解)
- ParNew 收集器和 Serial GC 的区别在于 ParNew 采用并行方式回收。 ParNew收集器在年轻代中同样也是采用复制算法、STW机制。
- 除Serial外,目前只有ParNew GC能与CMS收集器配合工作。
- 参数 -XX:+UseParNewGC 手动指定使用 ParNew 收集器执行内存回收任务。它表示年轻代使用并行收集器, 不影响老年代。
-XX:ParallelGCThreads 限制线程数量,默认开启和CPU数据相同的线程数。
对于新生代,回收次数频繁,使用并行方式高效。
对于老年代,回收次数少,使用串行方式节省资源。(CPU并行需要切换线程,串行可以省去切换线程的资源) - SerialGC 和 ParNewGC哪个更好?
- ParNew 收集器运行在多CPU的环境下,由于可以充分利用多CPU、多核心等物理硬件资源优势,可以更快速地完成垃圾收集,提升程序的吞吐量。
- 但是在单个CPU的环境下,ParNew收集器不比Serial 收集器更高效。虽然Serial收集器是基于串行回收,但是由于CPU不需要频繁地做任务切换,因此可以有效避免多线程交互过程中产生的一些额外开销。
5、Parallel、ParallelOld:吞吐量优先(Java8 默认)
-
HotSpot的年轻代中除了拥有 ParNew 收集器是基于并行回收的以外, Parallel Scavenge收集器同样也采用了复制算法、并行回收和"Stop the World"机制。
-
有了ParNew,Parallel收集器的出现是否多此一举?
- 和ParNew收集器不同,Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾收集器。
- 自适应调节策略也是Parallel Scavenge与ParNew一个重要区别。
-
高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。因此,常见在服务器环境中使用。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序。
-
Parallel 收集器在 JDK1.6 时提供了用于执行老年代垃圾收集的 Parallel Old收集器,用来代替老年代的Serial Old收集器
-
Parallel Old收集器采用了标记一压缩算法,但同样也是基于并行回收和“Stop一the一World”机制。
参数设置
-XX:+UseParallelGC:手动指定年轻代使用Parallel并行收集器执行内存回收任务-XX:+UseParallelOldGC:手动指定老年代都是使用并行回收收集器。
?? ? 分别适用于新生代和老年代。默认jdk8是开启的。上面两个参数,默认开启一个,另一个也会被开启。(互相激活)-XX:ParallelGCThreads:设置年轻代并行收集器的线程数。
一般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能
?? ? 在默认情况下,当CPU 数量小于8个, ParallelGCThreads 的值等于CPU 数量。
?? ? 当CPU数量大于8个,ParallelGCThreads 的值等于3+[5*CPU_Count]/8]-XX:MaxGCPauseMillis:设置垃圾收集器最大停顿时间(即STW的时间)。单位是毫秒
?? ? 为了尽可能地把停顿时间控制在MaxGCPauseMills以内,收集器在工作时会调整Java堆大小或者其他一些参数
?? ? 对于用户来讲,停顿时间越短体验越好。但是在服务器端,我们注重高并发,整体的吞吐量。所以服务器端适合Parallel进行控制
?? ? 该参数使用需谨慎-XX:+UseAdaptiveSizePolicy:设置Parallel Scavenge收集器具有自适应调节策略
?? ? 在这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,已达到在堆大小、吞吐量和停顿时间之间的平衡点
?? ? 在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMills),让虚拟机自己完成调优工作
6、CMS回收器:低延迟
6.1、CMS概述
- JDK1.5推出CMS (Concurrent-Mark-Sweep)收集器
它是HotSpot虚拟机中第一款真正意义上的并发收集器,第一次实现了让垃圾收集线程与用户线程同时工作。 - CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。 停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。
- CMS的垃圾收集算法采用标记一清除算法,并且也会" stop-the-world"。
- CMS 作为老年代的收集器,无法与JDK 1.4.0 中已经存在的新生代收集器Parallel Scavenge配合工作。所以在JDK 1. 5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个
参考资料:https://www.zhihu.com/question/43492129 - 在G1出现之前,CMS使用还是非常广泛的。CMS收集器在JDK9中被废弃,在JDK14中被移除。
6.2、CMS过程
CMS整个过程比之前的收集器要复杂,整个过程分为4个阶段,即初始标记阶段、并发标记阶段、重新标记阶段、并发清除阶段。
-
初始标记(Initial-Mark):仅仅只是标记出和GCRoots能直接关联到的对象,有STW现象、暂时时间非常短
-
并发标记(Concurrent-Mark):从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行(并发标记阶段有三色标记)
-
重新标记(Remark):由于在并发标记阶段中,程序的工作线程和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户线程继续运作而导致标记产生变动的那一部分对象的标记记录(有些对象可能开始是垃圾,在并发标记阶段,由于用户线程的影响,导致不是垃圾了,这里需要重新标记的是这部分对象),这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。
-
并发清除:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
-
补充:
- 在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
- CMS收集器的垃圾收集算法采用的是标记一清除算法,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块,不可避免地将会产生一些内存碎片。 那么CMS在为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer) 技术,而只能够选择空闲列表(Free List) 执行内存分配。
-
既然Mark Sweep会造成内存碎片,那么为什么不把算法换成Mark Compact呢?
答:因为当并发清除的时候,用Compact整理内存的话,原来的用户线程使用的内存将无发使用。要保证用户线程能继续执行,前提是它运行的资源不受影响。Mark Compact更适合 “Stop the World” 场景。
6.3、CMS优缺点
- 优点:并发收集、低延迟
- 缺点:
- 会产生内存碎片
- CMS收集器对CPU资源非常敏感。
在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低 - CMS收集器无法处理浮动垃圾。
可能出现"Concurrent Mode Failure" 失败而导致另一次Full GC的产生。在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间。
- 注意事项
- 并发标记阶段,在遍历GCRoots,用户线程也在执行,若此时遍历过一个对象发现没有引用,但由于用户线程并发执行,这期间可能导致遍历过的这个对象又被其他对象引用,所以才需要重新标记阶段再遍历一次看又没有漏标记的,否则就会导致被重新引用的对象被清理
- 浮动垃圾:在并发标记阶段一开始不是垃圾,最后变成了垃圾(属于多标的情况)
6.4、CMS参数设置
-XX:+UseConcMarkSweepGc:手动指定使用CMS收集器执行内存回收任务
?? ? 开启该参数后会自动将一XX: +UseParNewGc打开。即: ParNew (Young区用) +CMS (Old区用) +Serial Old的组合
-XX:CMSlnitiatingOccupanyFraction:设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收
?? ? JDK5及以前版本的默认值为68,即当老年代的空间使用率达到68%时,会执行一次CMS 回收。JDK6及以上版本默认值为92%
?? ? 如果内存增长缓慢,则可以设置一个稍大的值,大的阈值可以有效降低CMS的触发频率,减少老年代回收的次数可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。(作用:降低Full GC的执行次数)
-XX:+UseCMSCompactAtFullCollection:用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生。
?? ? 由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了
-XX:CMSFullGCsBeforeCompaction:设置在执行多少次Full GC后对内存空间进行压缩整理
-XX:ParallelCMSThreads:设置CMS的线程数量
?? ? CMS 默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads 是年轻代并行收集器的线程数。当CPU 资源比较紧张时,受到CMS收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕
7、重点:面试相关
重点关注各种常用的参数:设置内存的参数、设置垃圾回收器的参数
问:有哪些垃圾收集器,你对他们有什么了解?
答:不同的厂商会考虑使用不同的JVM,不同的JVM会使用不同的垃圾收集器,下面我介绍下主流的垃圾收集器有哪些(主流的7种),展开说明七种垃圾收集器的每一个细节。
- 截止JDK 1.8,一共有7款不同的垃圾收集器。每一款不同的垃圾收集器都有不同的特点,在具体使用的时候,需要根据具体的情况选用不同的垃圾收集器。
垃圾收集器 | 分类 | 作用位置 | 使用算法 | 特点 | 适用场景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 复制算法 | 响应速度优先 | 适用于单CPU环境下的client模式 |
ParNew | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多CPU环境Server模式下与CMS配合使用 |
Parallel | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 适用于后台运算而不需要太多交互的场景 |
Serial Old | 串行 | 老年代 | 标记-压缩算法 | 响应速度优先 | 适用于单CPU环境下的client模 |
Parallel Old | 并行 | 老年代 | 标记-压缩算法 | 吞吐量优先 | 适用于后台运算而不需要太多交互的场景 |
CMS | 并发 | 老年代 | 标记-清除算法 | 响应速度优先 | 适用于互联网或B/S业务 |
G1 | 并发、并行 | 新生代、老年代 | 标记压缩算法、复制算法 | 响应速度优先 | 面向服务端应用 |
-
不同厂商、不同版本的虚拟机实现差别很大。HotSpot 虚拟机在JDK7/8后所有收集器及组合(连线),如下图:
- 两个收集器间有连线,表明可以搭配使用。虚线表示已废除方案。
- Serial Old 作为 CMS 出现“Concurrent Mode Failure” 失败的后备方案。
- (红色虚线)由于维护和兼容性测试的成本,在 JDK8 时将Serial+CMS、ParNew+Serial Old这两个组合声明为Deprecated(JEP173),并在 JDK9 中完全取消了这些组合的支持(JEP214),即:移除。
- (绿色虚线)JDK14 中:弃用ParallelScavenga 和 SerialOld GC 组合(JEP 366)
- (青色虚线)JDK14 中:删除CMS垃圾回收器(JEP 363)
- GC发展阶段:Serial -> Parallel(并行)->CMS(并发)->G1 -> ZGC