为什么会有这篇文章?
**校招垃圾回收面试重灾区。**本人秋招面试过大大小小的公司,发现几乎所有公司在面试的时候,一旦涉及到JVM只是,CMS和G1是常问点,而且CMS问的尤其多。所以本文会对根据网上常见的资料做一个整合并根据自己面试过程中遇到的一些问题对CMS进行一些本质上的剖析。
**知识沉淀。**网上很多资料可能知识介绍可能很浅,只介绍CMS流程和优缺点,但是没有讲CMS怎么做到低停顿时间,以及和parallel scavenge在吞吐量方面的对比。本文想对这点进行相应的解答。
一. 思维导图
二. Parallel Scavenge
什么是Parallel Scavenge
与 ParNew 一样是多线程收集器。
其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
缩短停顿时间是以牺牲吞吐量和新生代空间来换取的: 新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。
可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不需要手动指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
老年带对应的事Parallel old
只需要记住关键字:多线程、吞吐量优先、jdk 1.8 默认、可自适应调节
优缺点
优点:高吞吐量
缺点:
- 垃圾回收时停顿时间可能过长:因为优先考虑了吞吐量,所以停顿时间可能会过长
- 不适合交互式的任务:因为停顿时间长
- 可能占用的内存比较高(为了保证高吞吐量):内存大,发生gc少,吞吐量高;但是因为内存大,单次gc复制算法时间复杂度高。
三. CMS
什么是CMS
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。
CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
CMS流程
需要注意的是老年代的收集采用的 **标记清除 **算法
- 初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;
- 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
- 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
- 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。
优缺点
优点:低停顿时间、并发清除
缺点:
- 吞吐量低: 低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
- 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
- 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
为什么采用标记清除算法
明明其他大多数垃圾回收器老年代都采用标记整理算法,为什么CMS要另辟蹊径采用标记清除?
标记清除速度快,但是会产生内存碎片
标记整理可以解决内存碎片问题,但是缺点就是整理移动的过程比较耗费时间。
在标记整理算法中,首先进行标记阶段,标记出所有存活的对象,然后进行整理阶段,将存活的对象向一端移动,然后释放整理后的空间。这个过程中,需要移动对象,可能会导致大量的内存复制和对象移动操作,对于老年代这样的大对象空间,移动对象的代价会相对较高,可能引起较长的暂停时间。
但恰恰我们的CMS是一个低停顿的垃圾回收器,所以选择了标记清除算法,容忍了内存碎片的问题。
什么是Concurrent Mode Failure
- 浮动垃圾:
由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。
CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。
(1)如果对象提升到年老代的速度太快,而CMS收集器不能保持足够多的可用空间时,就会导致年老代的运行空间不足;
(2)当年老代的碎片化达到某种程度,使得没有足够空间容纳从新生代提升上来的对象时,也会发生并发模式失败。
当发生并发模式失败时,年老代将进行垃圾收集以释放可用空间,同时也会整理压缩以消除碎片,这个操作需要停止所有的java应用线程,并且需要执行相当长时间。
如果发生了Concurrent Mode Failure ,就会垃圾回收器退化,由Serial old来进行垃圾回收,此时产生全局停顿,耗时较高,应避免发生。
四. 吞吐量与停顿时间
JVM的吞吐量是指在一定时间内,JVM能够执行的任务数量。
JVM的停顿时间则是指在执行任务的过程中,JVM需要停止执行任务来进行垃圾回收等操作所花费的时间。
JVM的吞吐量和停顿时间确实是存在一定的互斥关系的。
所以想要其中一个特性就需要舍弃到另外一个特性
影响因素
以下是影响CMS吞吐量和停顿时间的主要因素:
堆大小:较大的堆可以容纳更多的对象,减少垃圾回收的频率,从而提高吞吐量。但是,较大的堆也意味着更长的垃圾回收时间,可能导致较长的停顿时间。
并发线程数:CMS使用并发线程来执行标记和清除阶段。增加并发线程数可以提高并发执行的时间,减少停顿时间。但是,过多的并发线程可能会导致线程竞争和额外的系统开销,可能对吞吐量产生负面影响。
触发垃圾回收的阈值:CMS会根据触发垃圾回收的阈值来决定执行垃圾回收的时机。较低的阈值可能导致频繁的垃圾回收,增加停顿时间;而较高的阈值可能延迟垃圾回收,增加吞吐量但可能导致较长的停顿时间。
并发标记时间:并发标记阶段是CMS的核心,它需要在应用程序运行的同时进行垃圾对象的标记。较长的并发标记时间会增加停顿时间,而较短的并发标记时间会增加吞吐量。因此,并发标记的效率对吞吐量和停顿时间都有影响。
应用程序的行为:应用程序的内存分配模式、对象的生命周期等因素也会影响CMS的性能。如果应用程序产生了大量的垃圾对象或者有过多的存活对象,可能会增加垃圾回收的负担,导致较长的停顿时间。
如何平衡两者
一般来说,为了提高JVM的吞吐量,我们可以采取一些措施,比如增加并发线程数、调整垃圾回收策略等。但这些措施可能会增加JVM的停顿时间,因为在进行并发线程操作和垃圾回收操作时,JVM需要停止执行任务。
相反,为了减少JVM的停顿时间,我们可以采取一些措施,比如选择更加智能的垃圾回收策略、调整垃圾回收频率等。但这些措施可能会降低JVM的吞吐量,因为在进行智能垃圾回收和调整回收频率时,JVM需要占用更多的计算资源,导致无法执行更多的任务。
因此,在实际应用中,我们需要根据具体情况综合考虑吞吐量和停顿时间之间的关系,选择合适的JVM配置和垃圾回收策略。同时,我们也可以采用一些优化技巧,比如使用并发垃圾回收、调整堆大小等,来平衡吞吐量和停顿时间的关系。
五. CMS在停顿时间上做了哪些优化
- 与用户线程并发
- 采用标记清除算法
- 由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的
六. 常见的CMS参数
①. -XX:+UseConcMarkSweepGC: 手动指定使用CMS收集器执行内存回收任务
(开启该参数后会自动将一XX: +UseParNewGC打开。即: ParNew (Young区用) +CMS (0ld区用) +Serial 0ld的组合)
②. -XX:CMSlnitiatingOccupanyFraction:设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收
- JDK5及以前版本的默认值为68,即当老年代的空间使用率达到68%时,会执行一次CMS 回收。JDK6及以上版本默认值为92%
- 如果内存增长缓慢,则可以设置一个稍大的值,大的阈值可以有效降低CMS的触发频率,减少老年代回收的次数可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。因此通过该选项便可以有效降低Full GC的执行次数
③. -XX:+UseCMSCompactAtFullCollection:用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了
④. -XX:CMSFullGCsBeforeCompaction:设置在执行多少次Full GC后对内存空间进行压缩整理
⑤. -XX:ParallelCMSThreads:设置CMS的线程数量
- CMS 默认启动的线程数是(Parallel****GCThreads+3)/4
- ParallelGCThreads 是年轻代并行收集器的线程数。当CPU 资源比较紧张时,受到CMS收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕)
七.参考:
java全栈知识体系
javaguide
CMS为什么采用标记-清除算法
CMS低延迟垃圾收集器详解
jvm吞吐量和停顿时间是互斥的