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,循环此过程。
优点:
- 吞吐量高,一次遍历且是连续空间遍历(非指针引用);
- 没有内存碎片。
缺点:
- 需要额外复制空间;
- 高存活对象频繁复制,左手倒右手。
标记-清除算法
标记 -清除算法分为两个阶段。
- 根据GC ROOT标记出所有需要回收的对象;
- 统一回收掉所有被标记的对象。
优点:
- 空间利用率高。
缺点:
- 执行效率低,需要递归找出所有需要回收的对象;
- 内存碎片严重。
标记-整理算法
在标记-清除算法基础上,增加整理过程,牺牲执行效率,解决内存碎片问题。
分代收集算法
以上算法各有长短,JVM实行了分代收集垃圾(G1除外),新生代采用复制算法,老年代采取标记-清除/整理算法。
GC提供的实现
- Serial
- Parallel
- CMS
- G1
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:发生在新生代的GC。
- Major GC:发生在老年代的GC。
- Full GC:发生整体资源如Heap或者Metaspace不足。CMS是标记-整理,会出现内存不连续导致空间不可用,会触发Full GC。
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种中断的情况:
- 循环次数达到CMSMaxAbortablePrecleanLoops(默认0);
- CMSMaxAbortablePrecleanTime(默认5s)超时;
- CMSScheduleRemarkEdenPenetration(默认50%)Eden使用率。
CMS Final Remark(STW)
标记老年代被GC Roots直接引用或者被新生代引用的对象和dirty对象(preclean不能完全跟得上标记dirty对象),STW。 可以通过CMSScavengeBeforeRemark(默认未开启)开启强制触发Minor GC减少无效的新生代。
CMS-concurrent-sweep
清除未标记的老年代对象
CMS-concurrent-reset
为下一次Major GC做准备。
CMS常用调优参数
- CMSInitiatingOccupancyFraction:老年代达到触发的GC初始阈值,默认-1
- UseCMSInitiatingOccupancyOnly:只用初始阈值触发GC,默认false
- CMSScheduleRemarkEdenSizeThreshold:进入concurrent-abortable-preclean的触发阈值,默认2m
- CMSMaxAbortablePrecleanTime:中断CMSMaxAbortablePrecleanTime的超时时间,默认5s
- CMSScheduleRemarkEdenPenetration:中断CMSMaxAbortablePrecleanTime的Eden使用率,默认50
- CMSScavengeBeforeRemark:强制在Final Remark之前Minor GC,默认false
- UseCMSCompactAtFullCollection:是否在Full GC前进行压缩老年代,默认true
- CMSFullGCsBeforeCompaction:再经过多少次Full GC才压缩老年代,默认0,即此次Full GC压缩