JVM之GC梳理



简介

JVM的Garbage Collection简称GC,是Java的垃圾回收器,理解GC是非常重要。

GC算法

复制算法

复制算法将内存划分大小相同的S0和S1两块,每次只使用一块。当S0满时,将存活的对象复制到S1,并清空S0。当S1满时则同上所述。

复制算法改进,将内存划分为Eden、S0和S1三块,较大的为Eden,其中大小S0=S1。 当Eden满时,将存活的对象复制到S0,并清空Eden和S1。 下一次Eden满时,将存活的对象(包含之前存活的对象)复制到S1,并清空Eden和S0,循环此过程。

优点:

缺点:

标记-清除算法

标记 -清除算法分为两个阶段。

  1. 根据GC ROOT标记出所有需要回收的对象;
  2. 统一回收掉所有被标记的对象。

优点:

缺点:

标记-整理算法

在标记-清除算法基础上,增加整理过程,牺牲执行效率,解决内存碎片问题。

分代收集算法

以上算法各有长短,JVM实行了分代收集垃圾(G1除外),新生代采用复制算法,老年代采取标记-清除/整理算法。

GC提供的实现

Serial GC

Serial New复制算法、Serial Old标记-压缩;垃圾收集的过程(新生代和老年代)中会Stop The World(下文简称STW),远古时代的GC。

Parallel GC

ParNew GC复制算法、Parallel Old标记-压缩;只是Serial GC的多线程。 Parallel Scavenge for young GC是一个为了减少单次停顿时间的复制算法,牺牲了吞吐量,可和Serial/Parallel for Old GC配合。

CMS(Concurrent Mark and Sweep)

Concurrent Mark and Sweep是实现标记-清除算法且吞吐量最高的老年代GC,只能和ParNewGC配合,Serial Old为CMS备胎。是目前生产线上最合适的GC,下文重点分析。

G1(Garbage First)

G1和CMS设计目的一致,为了降低STW。G1没有采用分代算法,把内存分为一系列大小的区域,采用标记-整理算法,优先处理有用数据最少、垃圾最多的区域。 目前G1的吞吐量是败于CMS的,详情可以查看plumbr的测试,这里不做研究。

GC参数对应组合

参数 新生代 老年代
-XX:+UseSerialGC Serial New Serial Old
-XX:+UseParNewGC ParNew Serial Old
-XX:+UseParallelGC Parallel Scavenge Serial Old
-XX:+UseParallelOldGC Parallel Scavenge Parallel Old
-XX:+UseConcMarkSweepGC ParNew CMS/Serial Old

GC王者CMS

触发时机

Minor GC触发条件:Eden满则触发,若对象均存活空间不足则Minor GC promotion failed,进而promote到老年代。

Major GC触发条件:CMS触发有一个阈值,跟CMSInitiatingOccupancyFraction有关。 CMSInitiatingOccupancyFraction默认是-1,则会通过MinHeapFreeRatio和CMSTriggerRatio计算出阈值。 CMSInitiatingOccupancyFraction=(100-MinHeapFreeRatio)+(CMSTriggerRatio*MinHeapFreeRatio/100) =(100-40)+(80*40)/100=92。 一般是直接设置CMSInitiatingOccupancyFraction和UseCMSInitiatingOccupancyOnly,使阈值保持不变。

CMSInitiatingOccupancyFraction设置的太大老年代空间不足有可能造成CMS concurrent mode failure,此时用Serial Old备胎。 设置太小会让老年代提前Major GC,导致空间利用率低。

原理步骤

以下是我的测试程序,运行参数:

1
2
3
4
5
6
-Xms200m -Xmx200m -Xmn10m
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=70
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSScheduleRemarkEdenSizeThreshold=0
-XX:+PrintGCDetails
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CMSTest {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        while (true) {
            executorService.submit(() -> {
                for (int i = 0; i < 1000; i++) {
                    ThreadLocal<Integer> local = new ThreadLocal<>();
                    local.set(new Random().nextInt());
                }
            });
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
            }
        }
    }
}

相关GC LOG如下:

[GC (Allocation Failure) [ParNew: 9216K->1024K(9216K), 0.0349850 secs] 113588K->111113K(203776K), 0.0350356 secs] [Times: user=0.23 sys=0.00, real=0.03 secs]
[GC (Allocation Failure) [ParNew: 9216K->1024K(9216K), 0.0252930 secs] 119305K->117720K(203776K), 0.0253453 secs] [Times: user=0.25 sys=0.00, real=0.02 secs]
[GC (Allocation Failure) [ParNew: 9216K->1024K(9216K), 0.0110811 secs] 125912K->123976K(203776K), 0.0111366 secs] [Times: user=0.13 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [ParNew: 9216K->1024K(9216K), 0.0100200 secs] 132168K->130370K(203776K), 0.0100439 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [ParNew: 9216K->1023K(9216K), 0.0104503 secs] 138562K->136607K(203776K), 0.0104871 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [ParNew: 9215K->1022K(9216K), 0.0100852 secs] 144799K->142884K(203776K), 0.0101074 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (CMS Initial Mark) [1 CMS-initial-mark: 141862K(194560K)] 142927K(203776K), 0.0004697 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.040/0.040 secs] [Times: user=0.09 sys=0.00, real=0.04 secs]
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.006/0.006 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[CMS-concurrent-abortable-preclean-start]
[CMS-concurrent-abortable-preclean: 0.348/0.451 secs] [Times: user=0.36 sys=0.00, real=0.45 secs]
[GC (CMS Final Remark) [YG occupancy: 5162 K (9216 K)][Rescan (parallel) , 0.0012373 secs][weak refs processing, 0.0102569 secs][class unloading, 0.0004716 secs][scrub symbol table, 0.0003387 secs][scrub string table, 0.0002077 secs][1 CMS-remark: 141862K(194560K)] 147024K(203776K), 0.0145781 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.084/0.084 secs] [Times: user=0.08 sys=0.00, real=0.09 secs]
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

从LOG可以看出CMS有:

CMS Initial Mark(STW)

到达CMSInitiatingOccupancyFraction的阈值141862/194560>70%,触发标记老年代被GC Roots直接引用或者被新生代引用的对象,STW。

CMS-concurrent-mark

伴随应用线程并发递归在Initial Mark的基础上的对象并标记,此时应用程序可能会改变老年代对象的引用,造成不一致的引用,则在card(老年代的区间)上标记为dirty对象为下一阶段做准备。

CMS-concurrent-preclean

在CMS-concurrent-mark基础上整理处理dirty对象并递归标记可达的对象以及新生代新引用的老年代对象。

CMS-concurrent-abortable-preclean

此阶段为优化阶段,在Eden>CMSScheduleRemarkEdenSizeThreshold(默认2M)进入此阶段。我测试的时候设置为0只是为了让读者知道这个状态存在。 与上阶段作用大同小异,为了让下阶段充分准备好,让STW时间尽量短。 当Eden大于CMSScheduleRemarkEdenSizeThreshold就会等待,这个等待是可以中断的,有3种中断的情况:

  1. 循环次数达到CMSMaxAbortablePrecleanLoops(默认0);
  2. CMSMaxAbortablePrecleanTime(默认5s)超时;
  3. CMSScheduleRemarkEdenPenetration(默认50%)Eden使用率。

CMS Final Remark(STW)

标记老年代被GC Roots直接引用或者被新生代引用的对象和dirty对象(preclean不能完全跟得上标记dirty对象),STW。 可以通过CMSScavengeBeforeRemark(默认未开启)开启强制触发Minor GC减少无效的新生代。

CMS-concurrent-sweep

清除未标记的老年代对象

CMS-concurrent-reset

为下一次Major GC做准备。

参考plumbr.io

CMS常用调优参数