如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
一、串行
- 单线程
- 堆内存较小,适合个人电脑
- -XX: +UseSerialGC=Serial+SerialOld
- 新生代使用复制算法,老年代使用标记-整理算法,但工作模式相似
- 只使用一个CPU或一条收集线程去完成垃圾收集工作,必须暂停其他所有的工作线程,直到它收集结束。
二、吞吐量优先(单位时间STW的时间最短)
- 多线程
- 堆内存较大,需要多核cpu支持
- 单位时间内,stop the world时间最短,即运行用户代码的时间/CPU总消耗时间的比值最小。
- -XX:+UseParallelGC ~ -XX:+UseParallelOldGC两款搭配使用,开启其中一个另一个随即开启
- -XX:ParallelGCThreads=n调整参与垃圾回收的线程数
- -XX:+UseAdaptiveSizePolicy动态调整伊甸园和幸存者区的比例以及晋升阈值
- -XX:GCTimeRatio=ratio,ratio默认为99,含义为垃圾回收的时间不能超过总时间的0.01(1/(1+99)),根据这个指标调整堆大小(调大堆使GC频率下降)
- -XX:MaxGCPauseMillis=ms,最大垃圾回收时间,如果超过了这个时间那么将堆缩小。与上面的参数是矛盾的
三、响应时间优先(单次STW的时间最短)
- 多线程
- 堆内存较大,需要多核cpu支持
- 尽可能让单次stop the world时间最短
- -XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC(前者是老年代,基于标记-清除算法,后者是新生代,搭配使用,如果并发失败,随即从CMS垃圾回收器退化到SerialOld);Conc is short for Concurrent,即并发,在垃圾收集器的上下文语境中,并发是指用户线程和垃圾收集线程同时执行(但不一定同时执行),而并行是指多条垃圾收集线程并行工作,用户线程出于等待状态。
- 整个过程分为4个步骤,包括:初始标记、并发标记、重新标记、并发清除。初始标记很快,只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。耗时最长的并发标记和并发清除过程收集器线程可以与用户线程并行工作,所以整体上说,CMS收集器的内存回收过程与用户线程一起并发执行。
- -XX:ConcGCThreads=threads并发垃圾回收线程数,建议设置为cpu核心数的四分之一
- -XX:+CMSScavengeBeforeRemark在重新标记的时,新生代的对象可能会引用老年代的对象,但是有些新生代是要被马上回收的,为了避免做无用功,可以在发生Full GC的时候先进行一次Minor GC(ParNewGC)
CMS收集器有以下几个缺点:
- CMS收集器对CPU资源非常敏感,会占用CPU数量,降低程序的运行速度,在CPU数量低时尤其明显
- 无法处理浮动垃圾,所谓浮动垃圾就是CMS在进行并行清理的时候,伴随程序运行而产生的新的垃圾,这部分垃圾只能留待下一次GC时清理掉。由于垃圾收集阶段用户线程还需要运行,需要预留足够的内存空间给用户线程使用,通过 -XX:CMSInitiatingOccupancyFraction=percent设置触发CMS时的内存占比。如果占比过高,会导致“Concurrent Mode Failure”,虚拟机会临时启动Serial Old收集器来重新进行老年代垃圾收集,这样会导致停顿时间很长。
- 收集结束后会有大量空间碎片产生,对大对象分配带来很大麻烦。
四、G1垃圾收集器
4.1 适用场景
- 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是200ms
- 超大堆内存,会将堆划分为多个大小相等的Region,每个区域都可以被独立地作为Eden区,Survivor区以及老年代
- 整体是标记+整理算法,两个区域之间是复制算法
4.2 相关JVM参数
- -XX:+UseG1GC启用G1,在JDK1.9之后默认
- -XX:G1HeapRegionSize=size
- -XX:MaxGCPauseMillis=time
4.3 G1垃圾回收阶段
G1在进行垃圾回收时,会在这三种状态之间循环
-
Young Collection
新生代的垃圾回收有通过以下三张图来表示。新生代垃圾回收会发生Stop the world,在这个时候也会进行GC Root的初始化标记,并通过复制算法,将幸存者拷贝到幸存区;经过一段时间之后,再次发生新生代垃圾回收时,幸存区中的对象如果达到晋升年龄,则被移到老年区(O),如果年龄不够,且幸存,则连同Eden区中幸存对象一起被拷贝进幸存区
-
Young Collection+CM(Concurrent marking) 当老年代占用堆空间的比例到达阈值时,进行并发标记(不会STW),由下面的JVM参数决定阈值 -XX: InitiatingHeapOccupancyPercent=percent(默认45%)
-
Mixed Collection 会对E、S、O进行全面垃圾回收,在发生混合标记的时候,会进行最终标记,此时会Stop the world。对新生的垃圾回收如之前所述,但对老年代的垃圾回收则会根据设定的暂停时间,筛选回收价值最高的Region,如下图,虚框的O区都是需要回收的老年代,但是为了满足停顿要求,会有选择的筛选一部分,换句话说G1收集器是可预测停顿的。 -XX:MaxGCPauseMillis=ms可设置暂停时间
4.4 Young Collection跨带引用问题
如果老年代的对象对新生代有引用,那么要找新生代对象的根对象。在老年代中,根对象存活的比较多,那么在遍历的时候就要消耗大量的时间。为了加快时间,使用卡表与Remembered Set,如下图所示,老年代有一个卡表,其中粉红色区域即为存在跨代引用的区域,称之为脏卡,在新生代,有一个Remembered Set记录incoming reference,在对新生代进行垃圾回收时,即可通过Remembered Set找到脏卡,然后根据脏卡进行GC Root,节省了时间。
4.5 重新标记阶段
现在考虑这样一种情况,如下图所示,在并发标记阶段,C对象没有被强引用所引用,被当做垃圾,但是因为此时用户线程并发执行,又将A指向了C,G1如何处理这种情况呢?这时需要重新标记阶段。具体做法是当对象引用发生改变时,会触发一个写屏障(pre-write barrier),此时会将C对象放入一个名为satb_mark_queue的队列,之后会对其再判断。