JVM GC原理及调优的基本思路

和Web应用程序一样,Tomcat作为一个Java程序也跑在JVM中,因此如果要对Tomcat进行调优,需要先了解JVM调优的原理。而对于JVM调优来说,主要是JVM垃圾收集的优化,一般来说是因为有问题才需要优化,所以对于JVM GC来说,如果观察到Tomcat进程的CPU使用率比较高,并且在GC日志中发现GC次数比较频繁、GC停顿时间长,这表明需要对GC进行优化了。

在对GC调优的过程中,不仅需要知道GC的原理,更重要的是要熟练使用各种监控和分析工具,具备GC调优的实战能力。CMS和G1是时下使用率比较高的两款垃圾收集器,从Java 9开始,采用G1作为默认垃圾收集器,而G1的目标也是逐步取代CMS。

1.CMS vs G1

CMS收集器将Java堆分为年轻代(Young)或年老代(Old)。这主要是因为有研究表明,超过90%的对象在第一次GC时就被回收掉,但是少数对象往往会存活较长的时间。

CMS还将年轻代内存空间分为幸存者空间(Survivor)和伊甸园空间(Eden)。新的对象始终在Eden空间上创建。一旦一个对象在一次垃圾收集后还幸存,就会被移动到幸存者空间。当一个对象在多次垃圾收集之后还存活时,它会移动到年老代。这样做的目的是在年轻代和年老代采用不同的收集算法,以达到较高的收集效率,比如在年轻代采用复制-整理算法,在年老代采用标记-清理算法。因此CMS将Java堆分成如下区域:

与CMS相比,G1收集器有两大特点:

  • G1可以并发完成大部分GC的工作,这期间不会“Stop-The-World”。
  • G1使用非连续空间,这使G1能够有效地处理非常大的堆。此外,G1可以同时收集年轻代和年老代。G1并没有将Java堆分成三个空间(Eden、Survivor和Old),而是将堆分成许多(通常是几百个)非常小的区域。这些区域是固定大小的(默认情况下大约为2MB)。每个区域都分配给一个空间。 G1收集器的Java堆如下图所示:

图上的U表示“未分配”区域。G1将堆拆分成小的区域,一个最大的好处是可以做局部区域的垃圾回收,而不需要每次都回收整个区域比如年轻代和年老代,这样回收的停顿时间会比较短。具体的收集过程是:

  • 将所有存活的对象将从收集的区域复制到未分配的区域,比如收集的区域是Eden空间,把Eden中的存活对象复制到未分配区域,这个未分配区域就成了Survivor空间。理想情况下,如果一个区域全是垃圾(意味着一个存活的对象都没有),则可以直接将该区域声明为“未分配”。
  • 为了优化收集时间,G1总是优先选择垃圾最多的区域,从而最大限度地减少后续分配和释放堆空间所需的工作量。这也是G1收集器名字的由来——Garbage-First。

2.GC调优原则

GC是有代价的,因此调优的根本原则是每一次GC都回收尽可能多的对象,也就是减少无用功。因此在做具体调优的时候,针对CMS和G1两种垃圾收集器,分别有一些相应的策略。

CMS收集器

对于CMS收集器来说,最重要的是合理地设置年轻代和年老代的大小。年轻代太小的话,会导致频繁的Minor GC,并且很有可能存活期短的对象也不能被回收,GC的效率就不高。而年老代太小的话,容纳不下从年轻代过来的新对象,会频繁触发单线程Full GC,导致较长时间的GC暂停,影响Web应用的响应时间。

G1收集器

对于G1收集器来说,不推荐直接设置年轻代的大小,这一点跟CMS收集器不一样,这是因为G1收集器会根据算法动态决定年轻代和年老代的大小。因此对于G1收集器,需要关心的是Java堆的总大小(-Xmx)。

此外G1还有一个较关键的参数是-XX:MaxGCPauseMillis = n,这个参数是用来限制最大的GC暂停时间,目的是尽量不影响请求处理的响应时间。G1将根据先前收集的信息以及检测到的垃圾量,估计它可以立即收集的最大区域数量,从而尽量保证GC时间不会超出这个限制。因此G1相对来说更加“智能”,使用起来更加简单。

3.内存调优实战

下面通过一个例子实战一下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时才清空一次,用来模拟年老代对象。

2.用下面的命令启动测试程序:

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。

3.使用JMeter压测工具向程序发送测试请求,访问的路径是/greeting

4.使用GCViewer工具打开GC日志,可以看到这样的图:

解释一下这张图:

  • 图中上部的蓝线表示已使用堆的大小,看到它周期的上下震荡,这是因为对象池要扩展到200000才会清空。
  • 图底部的绿线表示年轻代GC活动,从图上看到当堆的使用率上去了,会触发频繁的GC活动。
  • 图中的竖线表示Full GC,从图上看到,伴随着Full GC,蓝线会下降,这说明Full GC收集了年老代中的对象。

基于上面的分析,可以得出一个结论,那就是Java堆的大小不够。解释一下为什么得出这个结论:

  • GC活动频繁:年轻代GC(绿色线)和年老代GC(黑色线)都比较密集。这说明内存空间不够,也就是Java堆的大小不够。
  • Java的堆中对象在GC之后能够被回收,说明不是内存泄漏。

通过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和G1两种垃圾收集器背后的设计思路以及它们的区别,接着分析了GC调优的总体原则。

对于CMS来说,要合理设置年轻代和年老代的大小。该如何确定它们的大小呢?这是一个迭代的过程,可以先采用JVM的默认值,然后通过压测分析GC日志。

如果看年轻代的内存使用率处在高位,导致频繁的Minor GC,而频繁GC的效率又不高,说明对象没那么快能被回收,这时年轻代可以适当调大一点。

如果看年老代的内存使用率处在高位,导致频繁的Full GC,这样分两种情况:如果每次Full GC后年老代的内存占用率没有下来,可以怀疑是内存泄漏;如果Full GC后年老代的内存占用率下来了,说明不是内存泄漏,要考虑调大年老代。

对于G1收集器来说,可以适当调大Java堆,因为G1收集器采用了局部区域收集策略,单次垃圾收集的时间可控,可以管理较大的Java堆。

参考

推荐阅读更多精彩内容

  • 这篇文章是我之前翻阅了不少的书籍以及从网络上收集的一些资料的整理,因此不免有一些不准确的地方,同时不同JDK版本的...
    高广超阅读 7,824评论 3 69
  • 转载blog.csdn.net/ning109314/article/details/10411495/ JVM工...
    forever_smile阅读 4,112评论 1 57
  • 作者:一字马胡 转载标志 【2017-11-12】 更新日志 日期更新内容备注 2017-11-12新建文章初版 ...
    beneke阅读 1,145评论 0 6
  • Java 虚拟机有自己完善的硬件架构, 如处理器、堆栈、寄存器等,还具有相应的指令系统。JVM 屏蔽了与具体操作系...
    尹小凯阅读 786评论 0 10
  • http://www.cnblogs.com/angeldevil/p/3801189.html值得一看 Clas...
    snail_knight阅读 528评论 1 0