当前位置: 首页 > 工具软件 > 71cms > 使用案例 >

CMS垃圾回收器详解

臧俊杰
2023-12-01

一、哪些对象可以回收

1、引用计数法

算法过程:每个对象有一个引用计数器,当对象被引用一次计数器就加一,引用失效就减一,对于计数器为0的对象表示为垃圾对象,可以被GC回收。

缺点:无法解决循环引用的问题,例如:A引用了B,B引用了A,但是A和B都没有被其他对象引用,这样就会导致内存泄漏,无法被回收。

2、可达性分析法

算法过程:
通过一系列被称为GC Roots的对象作为起点开始搜索,所经过的路径被称为引用链,当一个对象没有跟任何一个引用链相连接的时候,表示从GC Roots对象到这个对象不可达,意味着这是一个垃圾对象可以被回收。

适用场景:Java虚拟机是采用这种算法对垃圾进行回收,解决了循环引用的问题。

可以作为GC Roots对象有:虚拟机栈,静态成员,常量,本地方法栈引用的对象。

二、垃圾回收算法

1、标记清除

算法过程:首先标记出需要回收的对象,标记完成后统一回收。

缺点:主要有两个缺点,一个是标记和清除两个过程的效率都不算高(据资料显示)。另一个是空间问题,标记清除后,会产生不连续的内存碎片,当需要分配大对象时,无法找到足够连续的内存,导致分配失败提前触发GC。

2、复制算法

算法过程:将内存分为两部分,每次只使用其中一块内存。回收时将存活的对象复制到另一块区域,之后将已使用的内存区域一次性全部清理掉。

优点:解决了空间碎片的问题,存活对象少时,提升了回收效率。

缺点:一个是内存使用率缩小了,因为永远有一块空闲的内存备用。另一个是当存活对象较多时复制效率低下。

3、标记整理

算法过程:标记整理的算法过程跟标记清除的标记过程是一样的,但标记后是将存活的对象都想某一端移动,然后清理边界以外的内存。

优点:解决了内存碎片的问题。

4、分代收集

概念:JVM内存区域是分代的,分为:年轻代、老年代、永久代(元数据区)和两个幸存区。根据不同的算法特点,在不同分代中使用不同的算法进行垃圾回收。

年轻代、幸存区:由于Java对象在很多场景下,都是朝生夕死的,所以在这几个区间适合使用复制算法。

老年代、永久代:永久代或者元数据区用于存储一些类信息、常量池等数据,老年代一般也是存活比较久的对象,因此适合用标记清除和标记整理算法。

三、CMS介绍

1、介绍

老年代收集器,需要配合Serial或者ParNew使用,一般是与ParNew使用。CMS全称Concurrent Mark-Sweep。CMS出现的目标是为了降低延迟,减少回收停顿时间。适合对延迟、停顿时间敏感的应用使用。通过 -XX:+UseConcMarkSweepGC开启。

2、回收阶段

CMS-initial-mark(STW):通过可达性分析算法,从GC Roots对象开始扫描能够直接关联到的对象,并做标记,需要STW,但是一般会很快完成。

CMS-concurrent-mark:从初始标记的基础上,继续向下搜索并做标记,这个阶段应用线程和GC线程并发执行,不会造成停顿。

CMS-concurrent-preclean:查找并发标记阶段中,新进入老年代的对象,包括晋升到老年代和直接在老年代分配的对象,目的是减少下一个阶段重新标记的时间。

CMS-concurrent-abortable-preclean:可中断的预清理,这个阶段做的事情跟并发预清理的事情一样,目的是为了减少下一个阶段STW的时间,这个阶段有几个个条件控制何时结束。

A、-XX:CMSScheduleRemarkEdenSizeThreshold=2,该阶段在Eden区占用超过2M时启动。

B、-XX:CMSMaxAbortablePrecleanTime=5000,设置一个最大时间最大执行5秒钟。

C、-XX:CMSScheduleRemarkEdenPenetration=50,Eden区使用率超过50就停止该阶段进入remark阶段。

D、-XX:+CMSScavengeBeforeRemark,控制remark阶段前进行一次minor gc,来提高remark的效率,减少时间。

CMS-remark(STW):重新标记,处理上几次以来可能引用关系发生变化的部分,并重新进行标记,这个阶段会停止应用线程

CMS-concurrent-sweep:真正的清理阶段,将以上几个步骤标记的无法访问的对象进行并发清理,并将清理的空间回收到空闲列表中

CMS-concurrent-reset:调整堆大小,重置一些内部的数据结构,为下一次回收做准备

3、缺点

A、无法处理浮动垃圾,因为这是一款并发的收集器,程序运行和收集的同时都会产生垃圾,所以回收时不能向其他收集器一样等到满了的时候再收集,通过-XX:CMSInitiatingOccupancyFraction=65设置触发的百分比,留出一定的空间给并发收集的时候使用,当CMS运行期间无法满足程序的需要,则会出现,此时会临时使用Serial Old作为临时方案进行一次标记整理的回收,这样就会使得出现较为长期的停顿,所以-XX:CMSInitiatingOccupancyFraction=65不能设置太高。

B、由于是使用标记清除算法的收集器,因此会产生内存碎片,为了解决这个问题,垃圾收集器提供了一个-XX:+UseCMSCompactAtFullCollection的开关参数,表示再一次FGC时进行一次内存整理的过程,这个过程是无法并发的,会拉长停顿时间,收集器还提供另一个参数,-XX:CMSFullGCsBeforeCompaction=5表示FGC几次后进行一次内存整理。

4、优化措施

优化目标:减少STW停顿时间

优化手段:

A、降低触发CMS回收的百分比,给并发收集时留出一定的空间,避免Concurrent Mode Failure,通过-XX:CMSInitiatingOccupancyFraction=65配置

B、开启内存压缩,避免碎片问题无法分配大内存对象,通过-XX:+UseCMSCompactAtFullCollection和-XX:CMSFullGCsBeforeCompaction=5,具体数值可以按需调配。

C、减少CMS-remark(STW)阶段的时间,通过-XX:+CMSScavengeBeforeRemark、-XX:CMSMaxAbortablePrecleanTime=5000、-XX:CMSScheduleRemarkEdenPenetration=50进行微调。

5、相关参数
参数说明
-Xloggc:/dev/shm/xxx_gc.log设置GC日志路径
-XX:+PrintGCDateStamps打印GC时间
-XX:+PrintGCDetails打印GC详细信息
-XX:+PrintGCApplicationStoppedTime打印应用停顿时间
-XX:+PrintGCApplicationConcurrentTime打印应用当前时间
-XX:+PrintSafepointStatistics打印安全点统计信息
-XX:+UnlockDiagnosticVMOptions解锁诊断型参数
-XX:-DisplayVMOutput禁用控制台输出VM信息,例如安全点日志
-XX:+LogVMOutput启用日志输出VM日志
-XX:LogFile=/dev/shm/safepoint.log设置VM日志输出路径
-XX:CMSScheduleRemarkEdenPenetration=20设置Eden区使用率超过20%就进入remark阶段
-XX:CMSMaxAbortablePrecleanTime=1500设置可中断预清理阶段最大执行时间
-XX:+CMSScavengeBeforeRemark设置期望进入remark阶段前进行一次MinorGC
-XX:+PrintReferenceGC打印引用处理信息
-XX:+UseConcMarkSweepGC使用CMS收集器
-XX:+UseParNewGC使用ParNew收集器
-XX:+CMSParallelRemarkEnabledremark阶段采用并行多线程模式
-XX:+UseCMSCompactAtFullCollection开启压缩整理功能
-XX:CMSFullGCsBeforeCompaction=0设置CMS多少次后进行一次压缩
-XX:+CMSClassUnloadingEnabled开启回收永久代
-XX:+UseCMSInitiatingOccupancyOnly设置CMS回收时机一直按照Fraction配置进行
-XX:CMSInitiatingOccupancyFraction=60设置老年代内存使用率超过60时开始CMS回收
-XX:+AlwaysPreTouch启动时申请所需内存,避免系统调用延迟
6、日志分析

以下是一次完整的CMS回收日志,需要配置以下参数

2019-07-17T21:54:50.216+0800: 190007.616: [GC [1 CMS-initial-mark: 1258440K(2097152K)] 1294890K(3984640K), 0.0365050 secs] [Times: user=0.04 sys=0.01, real=0.04 secs]
2019-07-17T21:54:50.253+0800: 190007.654: Total time for which application threads were stopped: 0.0455330 seconds
2019-07-17T21:54:50.254+0800: 190007.654: [CMS-concurren
t-mark-start]
2019-07-17T21:54:50.413+0800: 190007.813: [CMS-concurrent-mark: 0.159/0.159 secs] [Times: user=0.91 sys=0.02, real=0.16 secs]
2019-07-17T21:54:50.413+0800: 190007.814: [CMS-concurrent-preclean-start]
2019-07-17T21:54:50.413+0800: 190007.814: [Preclean SoftReferences, 0.0005560 secs]2019-07-17T21:54:50.414+0800: 190007.814: [Preclean WeakReferences, 0.0011690 secs]2019-07-17T21:54:50.415+0800: 190007.815: [Preclean FinalReferences, 0.0000350 secs]2019-07-17T21:54:50.415+0800: 190007.815: [Preclean PhantomReferences, 0.0000070 secs]2019-07-17T21:54:50.425+0800: 190007.825: [CMS-concurrent-preclean: 0.011/0.011 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
2019-07-17T21:54:50.425+0800: 190007.826: [CMS-concurrent-abortable-preclean-start]
 CMS: abort preclean due to time 2019-07-17T21:54:51.938+0800: 190009.338: [CMS-concurrent-abortable-preclean: 1.508/1.512 secs] [Times: user=2.46 sys=0.16, real=1.51 secs]
2019-07-17T21:54:51.938+0800: 190009.338: Application time: 1.6846120 seconds
2019-07-17T21:54:51.946+0800: 190009.346: [GC[YG occupancy: 351451 K (1887488 K)]2019-07-17T21:54:51.946+0800: 190009.347: [GC2019-07-17T21:54:51.949+0800: 190009.350: [ParNew2019-07-17T21:54:51.979+0800: 190009.379: [SoftReference, 2284 refs, 0.0002550 secs]2019-07-17T21:54:51.979+0800: 190009.379: [WeakReference, 5 refs, 0.0000150 secs]2019-07-17T21:54:51.979+0800: 190009.379: [FinalReference, 679 refs, 0.0002280 secs]2019-07-17T21:54:51.979+0800: 190009.380: [PhantomReference, 0 refs, 0.0000120 secs]2019-07-17T21:54:51.979+0800: 190009.380: [JNI Weak Reference, 0.0000100 secs]: 351451K->23085K(1887488K), 0.0330260 secs] 1609892K->1282310K(3984640K), 0.0370030 secs] [Times: user=0.26 sys=0.01, real=0.04 secs]
2019-07-17T21:54:51.984+0800: 190009.384: [Rescan (parallel) , 0.0272010 secs]2019-07-17T21:54:52.011+0800: 190009.411: [weak refs processing2019-07-17T21:54:52.011+0800: 190009.411: [SoftReference, 5341 refs, 0.0020150 secs]2019-07-17T21:54:52.013+0800: 190009.413: [WeakReference, 0 refs, 0.0000220 secs]2019-07-17T21:54:52.013+0800: 190009.413: [FinalReference, 80 refs, 0.0002020 secs]2019-07-17T21:54:52.013+0800: 190009.414: [PhantomReference, 0 refs, 0.0000120 secs]2019-07-17T21:54:52.013+0800: 190009.414: [JNI Weak Reference, 0.0000230 secs], 0.0023470 secs]2019-07-17T21:54:52.013+0800: 190009.414: [class unloading, 0.0198310 secs]2019-07-17T21:54:52.033+0800: 190009.434: [scrub symbol table, 0.0100610 secs]2019-07-17T21:54:52.043+0800: 190009.444: [scrub string table, 0.0011130 secs] [1 CMS-remark: 1259224K(2097152K)] 1282310K(3984640K), 0.1204410 secs] [Times: user=0.48 sys=0.02, real=0.12 secs]
2019-07-17T21:54:52.067+0800: 190009.468: Total time for which application threads were stopped: 0.1293860 seconds
2019-07-17T21:54:52.068+0800: 190009.468: [CMS-concurrent-sweep-start]
2019-07-17T21:54:52.747+0800: 190010.147: [CMS-concurrent-sweep: 0.679/0.679 secs] [Times: user=1.24 sys=0.09, real=0.68 secs]
2019-07-17T21:54:52.747+0800: 190010.147: [CMS-concurrent-reset-start]
2019-07-17T21:54:52.753+0800: 190010.153: [CMS-concurrent-reset: 0.006/0.006 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]

通用字段解析

日志内容含义
2019-07-17T21:54:50.216+0800GC发生的时间,系统当前时间
190007.616虚拟机从启动到现在经过的时间
[Times: user=0.04 sys=0.01, real=0.04 secs]当前过程耗时情况,user表示运行用户代码时间,sys表示系统调用以及系统级等待的时间,real表示实际时间

CMS-initial-mark

日志内容含义
1258440K(2097152K) 1258440K回收前老年代占用大小,2097152K:老年代总大小
1294890K(3984640K) 1294890K整堆内存占用大小,3984640K:堆内存总大小

CMS-concurrent-mark

日志内容含义
[Preclean SoftReferences, 0.0005560 secs]该阶段处理软引用的时间
[Preclean WeakReferences, 0.0011690 secs]该阶段处理弱引用的时间
[Preclean FinalReferences, 0.0000350 secs]该阶段处理最终引用的时间
[Preclean PhantomReferences, 0.0000070 secs]该阶段处理虚引用的时间
[CMS-concurrent-preclean: 0.011/0.011 secs]预清理阶段耗费的时间

CMS-concurrent-abortable-preclean

日志内容含义
[CMS-concurrent-abortable-preclean: 1.508/1.512 secs]可中断预清理阶段耗费的时间,该阶段并行,不会STW

YG occupancy

日志内容含义
GC[YG occupancy: 351451 K (1887488 K)表示该阶段是MinorGC,351451表示年轻代占用大小,1887488年轻代总大小
351451K->23085K(1887488K), 0.0330260 secs351451K年轻代回收前大小,23085K年轻代回收后大小,1887488K年轻代总大小,耗时0.0330260

CMS-remark

日志内容含义
[Rescan (parallel) , 0.0272010 secs]重新标记花费的时间
[class unloading, 0.0198310 secs]类卸载花费的时间
[scrub symbol table, 0.0100610 secs]刷新符号表耗费的时间
[scrub string table, 0.0011130 secs]刷新常量池耗费的时间?
[1 CMS-remark: 1259224K(2097152K)]1282310K(3984640K), 0.1204410 secs] 1259224K老年代占用大小,2097152K老年代总大小,1282310K堆内存占用大小,3984640K堆内存总大小,耗时0.1204410s

四、实战

1、测试代码
/**
 * 堆溢出测试
 * -Xms100m -Xmx100m -XX:+UseConcMarkSweepGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/dump.hprof
 *
 * @author Horace
 */
public class HeapOOMTest {
    private static Logger logger = LoggerFactory.getLogger(HeapOOMTest.class);

    public static void main(String[] args) {
        int _1M = 1024 * 1024;
        List<byte[]> bytes = new ArrayList<>();
        while (true) {
            bytes.add(new byte[_1M]);
            LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1000));
        }
    }
}
2、查看GC情况

本地环境界面查看工具 jvisualvm
每1秒输出一次GC情况
jstat -gccause pid 1s

以下是GC情况

 S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT    LGCC                 GCC
 89.83   0.00  42.05  37.10  94.37  90.41      2    0.025     0    0.000    0.025 Allocation Failure   No GC
 89.83   0.00  46.58  37.10  94.37  90.41      2    0.025     0    0.000    0.025 Allocation Failure   No GC
 89.83   0.00  51.00  37.10  94.37  90.41      2    0.025     0    0.000    0.025 Allocation Failure   No GC
 89.83   0.00  54.74  37.10  94.37  90.41      2    0.025     0    0.000    0.025 Allocation Failure   No GC
 89.83   0.00  58.49  37.10  94.37  90.41      2    0.025     0    0.000    0.025 Allocation Failure   No GC
 89.83   0.00  63.69  37.10  94.37  90.41      2    0.025     0    0.000    0.025 Allocation Failure   No GC
 89.83   0.00  67.44  37.10  94.37  90.41      2    0.025     0    0.000    0.025 Allocation Failure   No GC
 89.83   0.00  71.19  37.10  94.37  90.41      2    0.025     0    0.000    0.025 Allocation Failure   No GC
 89.83   0.00  75.71  37.10  94.37  90.41      2    0.025     0    0.000    0.025 Allocation Failure   No GC
 89.83   0.00  79.46  37.10  94.37  90.41      2    0.025     0    0.000    0.025 Allocation Failure   No GC
 89.83   0.00  83.98  37.10  94.37  90.41      2    0.025     0    0.000    0.025 Allocation Failure   No GC
 89.83   0.00  87.72  37.10  94.37  90.41      2    0.025     0    0.000    0.025 Allocation Failure   No GC
 89.83   0.00  92.14  37.10  94.37  90.41      2    0.025     0    0.000    0.025 Allocation Failure   No GC

各个字段表示的含义可以查看:

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html#BEHHGFAE
命令行dump内存快照
jmap -heap dump:live,format=b,file=/tmp/dump.hprof
dump出来的内存可以通过jprofile、mat、jhat等工具进行分析

五、如何看JVM源码

1、源码下载地地址
http://jdk.java.net/java-se-ri/8
2、下载之后导入到IDEA中,cms垃圾收集器源码路径
hotspot/src/share/vm/gc_implementation/concurrentMarkSweep 包中
3、核心文件
hotspot/src/share/vm/gc_implementation/concurrentMarkSweep/concurrentMarkSweepGeneration.cpp
在这个文件的4550行,可以看到关于abortable_preclean阶段的实现 void CMSCollector::abortable_preclean()
 类似资料: