Home > Archives > Java虚拟机基础之CMS垃圾回收

Java虚拟机基础之CMS垃圾回收

Published on

在Java项目中,我们经常会在项目部署的时候打印垃圾回收(Garbage Collection)日志来帮助我们监控系统的运行状况。

java -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseConcMarkSweepGC \
-Xloggc:/Users/GC/jvm_gc.log -jar xxx/yyy.jar

在上面的配置中-XX:+UseConcMarkSweepGC表示垃圾回收使用的收集器,对应的还有-XX:+UseSerialGC-XX:+UseParallelGC等。并发标记-清除收集器(Concurrent Mark and Sweep Collector)是基于标记-清除算法的。这种算法的基本思路就是先对于堆中的对象进行标记,将能够通过GC roots访问到的对象标记live,然后在接下来的阶段对于没有标记的对象进行清除。

我们可以通过如下命令获取当前Java虚拟机使用的垃圾回收器

java -XX:+PrintCommandLineFlags -version

# 通过jps获取java对应的PID
jmap -heap PID

垃圾回收算法

对于垃圾回收算法,目前已经有很多实现(Parellel、CMS、G1),但大致会涉及到如下几个过程。

分代垃圾回收

目前大多数虚拟机的垃圾回收机制都采用了分代回收(Generational Collection)的思想,也就是将堆中的内存区域分为新生代和老生代,分别采用不同的机制进行垃圾回收,比如说对于对象存活率较低的新生代,我们可以采用使用复制算法的收集器(如Parallel New),只用付出复制一小部分对象的代价。而对于存活率较高的老生代,一般很难再开辟出空间进行复制,所以一般采用了使用标记-清除(如CMS)或者标记压缩(如G1)的收集器。

并发标记-清除垃圾收集(Concurrnt Mark Sweep Garbage Collection)

下面主要通过日志来了解并发标记-清除收集器的具体过程。

# JVM GC相关参数(jdk 1.8)
-XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps

在这种配置下,新生代采用Parallel New(ParNew, 标记复制)进行垃圾回收,老生代采用CMS(并发标记-清除)的方法进行垃圾回收。

# 新生代GC
2016-05-25T12:02:28.988+0800: 76791.792: [GC2016-05-25T12:02:28.988+0800: 76791.792: 
[ParNew: 857276K->24956K(943744K), 0.0295430 secs] 3540527K->2709351K(5138048K), 0.0298600 secs] 
[Times: user=0.10 sys=0.00, real=0.03 secs] 

# 初始标记阶段 - stop the world
2016-05-25T12:02:29.020+0800: 76791.824: 
[GC [1 CMS-initial-mark: 2684394K(4194304K)] 2712309K(5138048K), 0.0220000 secs] 
[Times: user=0.03 sys=0.00, real=0.03 secs] 

# 并发标记阶段  
2016-05-25T12:02:29.042+0800: 76791.847: [CMS-concurrent-mark-start]
2016-05-25T12:02:29.432+0800: 76792.236: [CMS-concurrent-mark: 0.381/0.390 secs] 
[Times: user=0.92 sys=0.01, real=0.39 secs] 

# 并发的预清理阶段
2016-05-25T12:02:29.432+0800: 76792.237: [CMS-concurrent-preclean-start]
2016-05-25T12:02:29.478+0800: 76792.282: [CMS-concurrent-preclean: 0.045/0.046 secs] 
[Times: user=0.05 sys=0.00, real=0.04 secs] 
2016-05-25T12:02:29.478+0800: 76792.282: [CMS-concurrent-abortable-preclean-start]
CMS: abort preclean due to time 2016-05-25T12:02:34.670+0800: 76797.475: 
[CMS-concurrent-abortable-preclean: 4.686/5.192 secs] 
[Times: user=4.89 sys=0.06, real=5.20 secs] 
 
# 重新标记阶段 - again stop the world
2016-05-25T12:02:34.671+0800: 76797.476: [GC (CMS Final Remark) [YG occupancy: 331133 K (943744 K)]
2016-05-25T12:02:34.671+0800: 76797.476: [Rescan (parallel) , 0.3236910 secs]
2016-05-25T12:02:34.995+0800: 76797.800: [weak refs processing, 0.1028850 secs]
2016-05-25T12:02:35.098+0800: 76797.903: [class unloading, 0.0291390 secs]
2016-05-25T12:02:35.127+0800: 76797.932: [scrub symbol table, 0.0159020 secs]
2016-05-25T12:02:35.143+0800: 76797.948: [scrub string table, 0.0018620 secs] 
[1 CMS-remark: 2684394K(4194304K)] 3015528K(5138048K), 0.5157410 secs] 
[Times: user=1.45 sys=0.01, real=0.51 secs] 


# 并发清理阶段
2016-05-25T12:02:35.189+0800: 76797.993: [CMS-concurrent-sweep-start]
2016-05-25T12:02:38.625+0800: 76801.430: [CMS-concurrent-sweep: 3.410/3.437 secs] 
[Times: user=3.73 sys=0.05, real=3.43 secs] 

# 并发重置阶段
2016-05-25T12:02:38.626+0800: 76801.430: [CMS-concurrent-reset-start]
2016-05-25T12:02:38.636+0800: 76801.440: [CMS-concurrent-reset: 0.010/0.010 secs] 
[Times: user=0.01 sys=0.01, real=0.01 secs]

上面展示的是部分垃圾回收日志,下面我们分阶段进行解释:

ParNew && CMS

新生代垃圾回收(Minor GC)

2016-05-25T12:02:28.988+0800: 76791.792: 
[
  GC (Allocation Failure) 2016-05-25T12:02:28.988+0800: 76791.792: 
  [ParNew: 857276K->24956 K(943744K), 0.0295430 secs] 
  3540527K->2709351K(5138048K), 0.0298600 secs
] 
[Times: user=0.10 sys=0.00, real=0.03 secs] 

垃圾回收完成之后,堆区内存占用从3540527变成了2709351,节省出831176; 新生代占用内存从857276变成了24956,节省出832320,这部分的对象被移动到了老生代。

老生代垃圾回收(Major GC)

(1) 初始标记(Initial Remark)

标记GC Roots能够直接关联的对象,会导致应用线程阻塞(stop-the-world)。

[GC [1 CMS-initial-mark: 2684394K(4194304K)] 2712309K(5138048K), 0.0220000 secs] 老生代占用内存(老生代分配内存) -> 堆占用内存(堆分配内存) -> 用时。

(2) 并发标记阶段(Concurrent Mark)

由前一阶段标记过的对象出发,开始追踪(trace)老生代所有可达对象并且开始标记,会和应用线程并行。

[CMS-concurrent-mark-start] ==> 并发标记开始 [CMS-concurrent-mark: 0.381/0.390 secs] [Times: user=0.92 sys=0.01, real=0.39 secs]

0.390secs -> 并发标记用时,后面的Times对于并发阶段的参考意义并不大,因为这个阶段还有很多其它的事发生。

(3) 并发的预清理阶段(Concurrent Preclean)

在并发标记的过程中,一些对象的引用已经发生改变,如果有对象的属性发生改变,则会被Java虚拟机标记为Dirty(也称之为Card Marking)。在预清理阶段,这些对象会被标记为live。该阶段为重新标记阶段提前做好一些准备。

(5) 可中止的并发预清理阶段(Concurrent Abortable Preclean)

这一过程与前一个过程有点类似,不过侧重点是分担一些最后标记阶段的任务。之所以叫可中止的是因为在某些条件满足的时候就不再进行预清理过程了(重复执行的次数达到上限,超过时间上限等)

(6) 最终标记(Final Remark)

同初始标记一样,也会导致应用线程阻塞。该阶段主要是对于老生代的对象(也包括之前被标记为Dirty的对象)再次遍历并进行标记。通常情况下会在新生代占用量尽可能小的情况下进行最后标记,这样可以避免连续的应用阻塞(stop-the-world)。

2016-05-25T12:02:34.671+0800: 76797.476: 
[
    GC (CMS Final Remark) [YG occupancy: 331133 K (943744 K)]
    2016-05-25T12:02:34.671+0800: 76797.476: [Rescan (parallel) , 0.3236910 secs]
    2016-05-25T12:02:34.995+0800: 76797.800: [weak refs processing, 0.1028850 secs]
    2016-05-25T12:02:35.098+0800: 76797.903: [class unloading, 0.0291390 secs]
    2016-05-25T12:02:35.127+0800: 76797.932: [scrub symbol table, 0.0159020 secs]
    2016-05-25T12:02:35.143+0800: 76797.948: [scrub string table, 0.0018620 secs] 
    [1 CMS-remark: 2684394K(4194304K)] 3015528K(5138048K), 0.5157410 secs
] 
[Times: user=1.45 sys=0.01, real=0.51 secs] 

(7) 并发清除(Conccurent Sweep)和重置(Conccurent Reset)

开始清理那些没有被标记为live对象,也就是Java虚拟机认为无用的对象。清除完成之后,进行CMS收集器内部数据结构的调整以准备下一阶段的GC。

参考

> Oracle官网对于CMS的介绍

> Plumbr Handbook Java Garbage Collection

> CMS GC日志概览

> 新生代空间分配失败(Allocation Failure)

> 为什么Full GC需要STW

声明: 本文采用 BY-NC-SA 授权。转载请注明转自: Allen写字的地方