陈同学
微服务
Accelerator
About
# 如何优化Java GC > 原文:[How to Tune Java Garbage Collection](https://www.cubrid.org/blog/how-to-tune-java-garbage-collection) by **Sangmin Lee** ON 06/02/2017 > 翻译:[陈同学](https://chenyongjun.vip/) > 参考:[JVM 调优 — GC 长时间停顿问题及解决方法](https://blog.csdn.net/wenniuwuren/article/details/51131741) 本文是 **成为Java GC专家** 系列的第三篇。 在第一篇 [理解 Java GC](https://chenyongjun.vip/articles/48) 中我们学习了不同GC算法的处理过程,GC是如何工作的,什么是年轻代和老年代,JDK7中的5种GC类型,以及每种GC类型对性能的影响。 在第二篇 [如何监控Java GC](https://chenyongjun.vip/articles/54) 中讲述了运行中的JVM如何进行GC,如何监控GC以及一些高效监控GC的工具。 本文将通过2个真实案例来演示一些你用得上的GC优化参数。本文假定你已理解本系列的前两篇文章,若还未阅读,请先阅读。 ## 有必要优化GC吗? 确切的说是 **基于Java的应用一定需要进行GC优化吗**?我认为并非所有基于Java的应用都需要进行GC优化,例如基于Java的系统有如下参数或行为: * 已经通过**-Xms** 和 **-Xmx** 指定了内存大小 * 包含了 **-server** 参数 * 系统中未出现 **超时** 等日志 **换句话说,如果你没有设置内存大小而且出现了大量超时日志,那么你需要在系统中进行GC优化了**。 但是有件事要铭记于心:**GC优化是你最后的手段**。 思考下GC优化的根本原因:Java中创建的对象由垃圾收集器来清理,同时待清理对象的数量和各类GC的执行次数又和创建对象总数量成正比。因此,为了控制GC的执行,首先要做的是 **减少创建对象的总数量**。 俗话说,"积少成多"。我们需要关注一些小事情,否则"养成气候"之后将难以驾驭。例如: * 使用StringBuilder或StringBuffer代替String * 尽可能少的输出日志 然而,有些场景我们也无能为力。我们知道,XML和JSON的解析会消耗大量的内存,就算尽可能少的使用String和日志也作用不大,因为还是会使用大量的临时内存来解析它们,有时甚至是10-100M。但是,又不太可能不使用XML和JSON,只能任由内存被消耗。 如果在几次参数调整后内存使用情况有所改善,你就可以进行GC优化了。我将GC优化的目的分成两类: * 将转移到老年代的对象数量降到最少 * 减少Full GC的执行时间 **将转移到老年代的对象数量降到最少** Oracle JVM提供了分代垃圾回收机制(JDK1.7及以上的G1 GC除外)。换句话说,对象创建在Eden区,然后在Survivor的From和To区之间移动,最后存活的对象被转移到老年代。一些大对象在Eden区创建之后被直接转移到老年代。相对新生代,老年代的GC消耗的时间更长。因此,减少从新生代转移到老年代的对象数量可以降低Full GC的频率。 减少从新生代转移到老年代对象的数量的说法容易造成误解,而且也不可能,但可以通过 **调整年轻代的大小** 来实现。 **减少Full GC的时间** 和Minor GC相比,Full GC的执行时间长很多。因此,如果执行Full GC的时间过长(超过1s),将导致连接服务的请求超时。 * 如果通过减少老年代的大小来降低Full GC执行时间,会造成OutOfMemoryError或增加Full GC的次数 * 如果增大老年代大小以期减少Full GC的执行次数,那么执行时间又会增加 因此,需要合理的设置 **老年代大小**。 ## 影响GC性能的参数 我在 [理解 Java GC](https://chenyongjun.vip/articles/48) 中提到过,不要去想 "**有人在使用一些GC参数后性能显著提升,为什么我们不使用相同的参数?**",原因是 **不同Web应用中对象的大小和生命周期不同**。 对于Java GC参数的设置,设置多个参数并不会提高GC的执行速度,恰恰相反,可能会降低执行速度。GC优化的基本原则是:**将不同的GC参数应用到2个或多个主机,然后比对结果,最后将性能最优的参数组合推广到其他主机**,这点必须铭记于心。 下表是一些影响GC性能的参数。 > 表1: GC优化时需要检查的JVM参数 | 分类 | 参数 | 描述 | | ------ | ------------------- | -------------------------- | | 堆区 | `-Xms` | 启动JVM时的初始堆大小 | | | `-Xmx` | 最大堆内存 | | 新生代 | `-XX:NewRatio` | 新生代和老年代内存大小比例 | | | `-XX:NewSize` | 新生代大小 | | | `-XX:SurvivorRatio` | Eden和Survivor区的比率 | 我经常使用 **-Xms、-Xmx、-XX:NewRatio** 三个参数来进行GC调优。**-Xms、-Xmx** 是肯定需要的,**-XX:NewRatio** 的设置将会显著的影响GC性能。 有的人可能会问 **如何设置Perm区大小**? 你可以通过 **-XX:PermSize、-XX:MaxPermSize** 设置,这个会与Perm区 **OutOfMemoryError**相关。 另一个会影响GC性能的是 GC类型,下面是基于JDK1.6可选的GC类型: | 类别 | 参数 | 备注 | | ---------------------- | ------------------------------------------------------------ | ------------------------------ | | Serial GC | -XX:+UseSerialGC | | | Parallel GC | -XX:+UseParallelGC -XX:ParallelGCThreads=value | | | Parallel Compacting GC | -XX:+UseParallelOldGC | | | CMS GC | -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:CMSInitiatingOccupancyFraction=value -XX:+UseCMSInitiatingOccupancyOnly | | | G1 | -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC | 在JDK6,这两个参数必须同时使用 | 除G1 GC外,GC类型可以通过第一行的参数来切换。最常见的GC类型是 Servial GC,它针对客户端系统专门进行了优化。 影响GC性能的参数有很多,但是上面的参数有着最为显著的影响。记住,设置过多的参数并不能保证一定会缩短GC的时间。 ## GC优化的步骤 GC优化过程与一般的性能优化类似,下面是我进行GC优化的步骤。 1. **监控GC状态** 你需要监控和检查运行中系统的GC状态,监控方式请参考 [如何监控Java GC](https://chenyongjun.vip/articles/54) 。 2. **根据分析结果决定是否需要GC优化** 在检查GC状态后,你需要分析监控结果,再决定是否进行GC优化。如果分析结果显示GC的时间只不过是0.1-0.3s,就不要浪费时间搞什么GC优化了。**然而,如果GC时间有1-3s,甚至超过10s,那GC势在必行**。 但是,如果你已经分配了10G Java内存,而且没有办法降低内存大小的话,就没办法进行GC优化了。在GC优化之前,你需要思考下为什么需要分配这么大的内存。如果只是分配了1-2G内存并且发生了OutOfMemoryError,你需要获取heap dump来验证和排查原因。 3. **设置GC类型和内存大小** 如果你已经决定要进行GC优化,同时,如果你拥有多台机器,一定要检查不同GC参数对不同机器的影响。 4. **分析结果** 设置GC参数后让程序运行至少24小时,再分析收集到的数据。幸运的话,你可能立马找到对系统最合适的GC参数,否则你需要分析日志,检查内存分配情况,然后再通过调整GC类型和内存大小来找到最佳参数配置。 5. **如果结果满意,将参数应用到所有机器并停止优化** 在下面的部分,你将看到每一步的具体步骤。 ### 监控GC状态&分析结果 检查Web应用GC状态最好的方式是使用 **jstat** 命令。下面的例子是GC优化之前状态。 ![](https://blog-1256695615.cos.ap-shanghai.myqcloud.com/2018/07/07/dc442eb9e65c48a39c8c486666114862.png) 检查YGC和YGCT,YGCT/YGC=0.050s(50ms),这意味着执行Minor GC的平均时间为50ms,你可以不用关注年轻代的GC情况。 现在,检查FGC和FGCT。FGCT/FGC=19.68s,这意味着GC的平均时间为19.68s。这可能是3次时间都为19.68的GC,也有可能是两次GC总耗时1秒而另一次GC耗时58秒,不过这两种情况都需要进行GC优化。 虽然可以通过**jstat**方便的获取GC状态,但分析GC最好的方式是通过**-verbosegc** 参数产生gc日志。如果GC执行时间符合下面所有条件,那没必要进行GC优化: * Minor GC执行很快(少于50ms) * Minor GC执行不是很频繁(大概10秒/次) * Full GC执行很快(少于1s) * Full GC执行不是很频繁(10分钟/次) 上述的值也不是绝对的,这取决于服务的状态。有的服务Full GC可能只要0.9秒,有的可能长点。因此,是否执行GC优化也要考虑具体的场景。 ### 设置GC类型和内存大小 #### 设置GC类型 Oracle JVM提供了5种GC类型,如果不是JDK7,可以在Parallel GC、Parallel Compacting GC、CMS GC中选择一种,到底选哪种也没什么特殊的规则。 那么,**我到底该选哪种呢?** 最推荐的方式是三种都试试。不过可以明确的是CMS GC比两种Parallel GC要快,如果测试CMS GC确实较快,直接使用CMS GC即可。但是,CMS GC也不总是最快的,通常来说,CMS GC执行Full GC的话会快点,不过一旦出现并发模式失败,Parallel GC会更快点。 #### 并发模式失败 让我们了解下并发模式失败。 CMS GC和Parallel GC之间最大的区别是 **压缩** 任务,压缩任务指的是移除内存碎片。 在Parallel GC中,在Full GC之后需要执行压缩任务,因此GC时间更长。但是,在Full GC之后,由于可以连续分配内存,内存分配速度会更快。 CMS GC恰恰相反,Full GC后它不会执行压缩任务。因此,CMS GC执行的更快,不过由于未执行压缩任务,也会产生许多的内存碎片,可能导致无法为大对象分配内存。例如,老年代剩下300M,一些10M的大对象又无法被连续分配,在这种场景下,会发生 "**并发模式失败**" 警告并执行压缩任务。需要注意,CMS GC的压缩时间比其他Parallel GC时间要长很多,而且可能导致其他问题。更多关于并发模式失败的信息,可以参考Oracle工程师写的 [理解CMS GC日志](https://blogs.oracle.com/poonam/understanding-cms-gc-logs)。 最终结论是,每个系统需要其合适的GC类型,你需要为你的系统找到合适的GC类型。如果你运行了6台主机,我建议你每两台设置相同的参数并添加 **-verbosegc**,然后分析监控结果。 #### 设置内存大小 下面展示了内存大小和GC执行次数及时间的关系。 * 大内存空间 * 降低了GC执行次数 * 增加了GC执行时间 * 小内存空间 * 增加了GC执行次数 * 降低了GC执行时间 内存到底设置小点还是大点并没有标准答案,如果机器资源充足而且Full GC能在1秒以内搞定的话,哪怕内存设置成10G也是可以的。但多数情况下,10G的内存Full GC会消耗10-30s,当然,时间也取决于对象的大小。 那么,**内存大小该怎么设置**?一般我推荐500M,不过注意并不是让你把Web应用的内存直接设置成**-Xms500m -Xmx500m**。在GC优化之前,先检查Full GC之后的内存剩余大小,如果剩下300M,那内存可以设置为1G(300M(默认使用的内存) + 500M(老年代最小内存) + 200M(自由空间)),这意味着老年代至少要设置500M以上。因此,如果你有3台主机,可以将内存分别设置为1G、1.5G、2G来试试效果。 为了配置内存大小你还需要设置**NewRatio**,**NewRatio**是新生代和老年代的比率。如果 **XX:NewRatio=1**,那新生代与老年代大小是1:1,如果堆内存是1G的话,那新生代为500M,老年代也是500M。如果**XX:NewRatio=2**,那新生代:老年代=1:2。因此,NewRatio值越大,老年代的大小就越大,新生代则越小。 NewRatio的值将显著影响整个GC的性能。如果新生代太小,很多对象将转移到老年代,导致频繁的Full GC同时也会增加Full GC的时间。 你可能会想 **NewRatio=1** 最好,不过事实并非如此,就我见过的案例来说, **NewRatio设置为2或3时整个GC状态会更好些**。 **最快完成GC优化的方式是怎么样的呢**?比对不同性能测试的结果是最快的方式。为不同机器设置不同的参数,推荐运行1-2天后再检查数据。但是,进行GC优化时,要确保使用了想同的负载,如:请求的频率和URL都应该一致。不过,由于即使是专业测试人员想控制相同的负载也很苦难,需要花费大量时间准备。因此,相对比较简单的方式是调整参数,然后花费较长的时间来收集结果。 ### 分析GC优化结果 在设置GC参数以及**-verbosegc**参数之后,通过tail命令确保日志被正确的生成。如果参数设置的不正确导致日志没有生成,你就是在浪费时间。如果日志正确的话,持续收集1到2天之后再检查结果。最简单的方式是将日志下载到本地并用**HPJMeter**来分析。 分析过程中,关注以下指标。优先级是我个人排列的,决定GC参数最重要的指标是Full GC的执行时间。 - Full GC 执行时间 - Minor GC执行时间 - Full GC 执行间隔 - Minor GC 执行间隔 - Entire Full GC 执行时间 - Entire Minor GC 执行时间 - Entire GC 执行时间 - Full GC e执行时间 - Minor GC 执行时间 能找到最佳的GC参数是件非常幸运的事情,然而在大多数场合,我们并不能如愿。在进行GC优化时要尽量小心谨慎,如果想一步到位搞定优化,往往会导致OutOfMemoryError 。 ## 优化例子 到目前为止,我们都在纸上谈兵,现在让我们看看GC优化的案例。 ### 案例1 这个例子是Full GC时间较长。 通过 *jstat -gcutil* 获取如下数据: ``` S0 S1 E O P YGC YGCT FGC FGCT GCT 12.16 0.00 5.18 63.78 20.32 54 2.047 5 6.946 8.993 ``` Perm区域对于首次GC优化来说并不重要,当前,YGC的值更有价值。 下面是Minor GC和Full GC 的相关数据: | GC类型 | GC执行次数 | GC执行时间 | 平均时间 | | -------- | ---------- | ---------- | -------- | | Minor GC | 54 | 2.047 | 37 ms | | Full GC | 5 | 6.946 | 1,389 ms | 37ms对于Minor GC来说不算坏,然而,1.389s的Full GC意味着在GC时系统会频繁超时。 首先,你需要检查在GC之前内存是如何使用的。通过 `jstat –gccapacity` 检查内存使用情况,结果如下: ![](https://blog-1256695615.cos.ap-shanghai.myqcloud.com/2018/07/07/6516e6a135084f3aa9a51a6de5b28bc3.png) 关键数据如下: - New area usage size: 212,992 KB - Old area usage size: 1,884,160 KB 因此,不算Perm区域的话,已分配的总内存是2G,New area:Old area比率为1:9。通过添加 **-verbosegc** 日志来获取更详细的日志,以下三个选项分别设置在不同机器上,且没有添加其他参数: - NewRatio=2 - NewRatio=3 - NewRatio=4 一天之后,获取GC日志。幸运的是,在设置NewRatio之后并为发生Full GC。 **为什么呢**?原因是大多数对象创建之后就销毁了,对象不用从新生代移动到老年代。 在这种状态下,不用改变其他JVM参数,选择一个合适的 **NewRatio**值即可。那么,我们怎么决定最佳的NewRatio值呢?我们可以分析下不同NewRatio值下的每次Minor GC的平均时间,数据如下: - NewRatio=2: 45 ms - NewRatio=3: 34 ms - NewRatio=4: 30 ms 现在我们可以下结论了,由于新生代最小,GC时间最短,NewRatio=4是最佳的选择。在使用该选项后,服务器没有再发生Full GC。 为了说明这个问题,下面是服务运行一段时间后执行jstat –gcutil的结果: ``` S0 S1 E O P YGC YGCT FGC FGCT GCT 8.61 0.00 30.67 24.62 22.38 2424 30.219 0 0.000 30.219 ``` 你可能会认为因为服务器接受的请求少才导致的GC执行频率下降。实际上,虽然Full GC没有执行,但是Minor GC被执行了 2424次。 ### 案例2 我们通过公司内部的性能管理系统(APM)发现JVM暂停了相当长的时间(超过8s),因此我们进行了GC优化。我们找到了Full GC时间长的原因并决定进程GC优化。 首先我们添加了 **-verbosegc** 参数,下面是结果: ![](https://blog-1256695615.cos.ap-shanghai.myqcloud.com/2018/07/08/fa7b39a6cf8947c992d0df150bad8585.jpeg) 上图是HPJMeter根据分析结果自动生成的图片,X轴表示JVM启动之后的时间,Y轴表示每次GC的时间。绿色的点是CMS GC,代表Full GC的结果,蓝色的点表示Parallel Scavenge,代表Minor GC结果。 前面我们说过CMS GC时间是最快的,但是上面的结果显示有些GC花费的时间超过15秒。**到底是什么导致了这个结果**?记住前面我说过:在压缩任务执行是CMS会更慢点,另外,这个程序内存设置为`–Xms1g` and `–Xmx4g`,已分配的内存也达到了4G。 因此我将GC类型从CMS GC改成了Parallel GC,并将内存改成了2G,NewRatio设置为3。几个小时之后,通过 **jstat -gcutil** 得到的结果如下: ``` S0 S1 E O P YGC YGCT FGC FGCT GCT 0.00 30.48 3.31 26.54 37.01 226 11.131 4 11.758 22.890 ``` Full GC时间稍微快了一点,相对4GB时的15秒,现在平均每次为3秒。但是3秒一样比较慢,因此我设计了如下6种场景。 - Case 1: -XX:+UseParallelGC -Xms1536m -Xmx1536m -XX:NewRatio=2 - Case 2: -XX:+UseParallelGC -Xms1536m -Xmx1536m -XX:NewRatio=3 - Case 3: -XX:+UseParallelGC -Xms1g -Xmx1g -XX:NewRatio=3 - Case 4: -XX:+UseParallelOldGC -Xms1536m -Xmx1536m -XX:NewRatio=2 - Case 5: -XX:+UseParallelOldGC -Xms1536m -Xmx1536m -XX:NewRatio=3 - Case 6: -XX:+UseParallelOldGC -Xms1g -Xmx1g -XX:NewRatio=3 **那一个最快呢?**结果显示,内存越小,结果越好。下图展示了Case6的结果,这是GC的性能提升最高的。最长的响应时间只有1.7秒,平均时间在1秒之内。 ![](https://blog-1256695615.cos.ap-shanghai.myqcloud.com/2018/07/08/8b3f512a72dd43aeb8b7ff96e17ed78a.png) 基于以上结果,我按照Case6调整了GC参数。但是,这导致每天晚上都发生OutOfMemoryError。在这里很难解释具体的原因,简而言之,批数据处理程序导致了内存泄漏,现在相关的问题已经被解决。 如果GC日志的分析时间很短,然后就将优化结果应用到所有服务器是非常危险的。必须牢记,只有在结合GC日志和应用程序进行分析之后才有可能优化成功。 我们了解了两个关于GC优化的例子,正如我之前提到的,例子中提到的GC参数,可以设置在相同服务器(CPU、操作系统、JDK版本、运行的服务相同)之上。 ## 结论 我并未获取heap dump文件,只是凭借经验进行GC优化,精确地分析内存可以得到更好的优化效果。不过这种分析一般适用于内存使用量相对固定的场景。但是,如果服务严重过载并占用的大量的内存,建议你根据上面的经验进行GC优化。
本文由
cyj
创作,可自由转载、引用,但需署名作者且注明文章出处。
文章标题:
如何优化Java GC
文章链接:
https://chenyongjun.vip/articles/56
扫码或搜索 cyjrun 关注微信公众号, 结伴学习, 一起努力