若观察到Tomcat进程CPU使用率较高,并在GC日志中发现GC次数比较频繁、GC停顿时间长,说明需优化GC。
CMS和G1是时下使用率比较高的两款垃圾收集器,从Java 9开始,G1是默认垃圾收集器。
将Java堆分为新生代(Young)或老年代(Old),因为研究表明,超过90%的对象在第一次GC时就被回收掉,仅少数对象会存活较长。
CMS还将新生代内存空间分为幸存者空间(Survivor)和伊甸园空间(Eden):
当一个对象在多次垃圾收集之后还存活时,它会移动到年老代。这样做的目的是在年轻代和年老代采用不同的收集算法,以达到较高的收集效率,比如在年轻代采用复制-整理算法,在年老代采用标记-清理算法。
与CMS相比,G1收集器有两大特点:
U表示“未分配”区域。G1将堆拆分成小的区域:可以做局部垃圾回收,而无需每次都回收整个区域,这样回收的停顿时间会比较短。
GC有代价,因此根本原则是每次GC都回收尽可能多的对象。 针对CMS和G1有相应策略。
最重要的是合理地设置年轻代和年老代大小。
不推荐直接设置年轻代大小,和CMS不不同,因为G1会根据算法动态决定年轻代和年老代大小。 因此对于G1,最关心Java堆总大小(-Xmx)。
-XX:MaxGCPauseMillis = n
限制最大GC暂停时间,以尽量不影响请求的响应时间。G1将根据先前收集信息及检测到的垃圾量,估计它可以立即收集的最大区域数量,从而尽量保证GC时间不会超出这个限制。因此G1更“智能”,使用更简单。
下面我通过一个例子实战一下Java堆设置得过小,导致频繁的GC,我们将通过GC日志分析工具来观察GC活动并定位问题。
1.首先我们建立一个Spring Boot程序,作为我们的调优对象
@RestController
public class GcTestController {
private Queue<Greeting> objCache = new ConcurrentLinkedDeque<>();
@RequestMapping("/greeting")
public Greeting greeting() {
Greeting greeting = new Greeting("Hello World!");
if (objCache.size() >= 200000) {
objCache.clear();
} else {
objCache.add(greeting);
}
return greeting;
}
}
@Data
@AllArgsConstructor
class Greeting {
private String message;
}
就是创建了一个对象池,当对象池中的对象数到达200000时才清空一次,用来模拟年老代对象。
命令启动测试程序:
java -Xmx32m -Xss256k -verbosegc -Xlog:gc*,gc+ref=debug,gc+heap=debug,gc+age=trace:file=gc-%p-%t.log:tags,uptime,time,level:filecount=2,filesize=100m -jar target/demo-0.0.1-SNAPSHOT.jar
我给程序设置的堆的大小为32MB,目的是能让我们看到Full GC。除此之外,我还打开了verbosegc日志,请注意这里我使用的版本是Java 12,默认的垃圾收集器是G1。
上部的蓝线表示已使用堆大小,周期上下震荡,这是对象池要扩展到200000才会清空。 绿线表示新生代GC活动,当堆使用率上去了,会触发频繁GC活动。 竖线表示Full GC,伴随着Full GC,蓝线会下降,这说明Full GC收集了老年代中的对象。
综上,Java堆大小不够:
GCViewer还发现累计GC暂停时间有55.57秒:
因此我们的解决方案是调大Java堆的大小,像下面这样:
java -Xmx2048m -Xss256k -verbosegc -Xlog:gc*,gc+ref=debug,gc+heap=debug,gc+age=trace:file=gc-%p-%t.log:tags,uptime,time,level:filecount=2,filesize=100m -jar target/demo-0.0.1-SNAPSHOT.jar
生成的新的GC log分析图如下:
你可以看到,没有发生Full GC,并且年轻代GC也没有那么频繁了,并且累计GC暂停时间只有3.05秒。
CMS来说,我们要合理设置年轻代和年老代的大小。你可能会问该如何确定它们的大小呢?这是一个迭代的过程,可以先采用JVM的默认值,然后通过压测分析GC日志。
如果我们看年轻代的内存使用率处在高位,导致频繁的Minor GC,而频繁GC的效率又不高,说明对象没那么快能被回收,这时年轻代可以适当调大一点。
如果我们看年老代的内存使用率处在高位,导致频繁的Full GC,这样分两种情况:如果每次Full GC后年老代的内存占用率没有下来,可以怀疑是内存泄漏;如果Full GC后年老代的内存占用率下来了,说明不是内存泄漏,我们要考虑调大年老代。
对于G1收集器来说,我们可以适当调大Java堆,因为G1收集器采用了局部区域收集策略,单次垃圾收集的时间可控,可以管理较大的Java堆。
设置过大,回收频率会降低,导致单次回收时间过长,因为需要回收的对象更多,导致GC stop the world时间过长,引起GC停顿时间过长,导致请求无法及时处理