前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如何优化Java GC

如何优化Java GC

作者头像
码代码的陈同学
发布2018-07-08 11:14:37
1.1K0
发布2018-07-08 11:14:37
举报

欢迎访问 陈同学博客原文 原文:How to Tune Java Garbage Collection by Sangmin Lee ON 06/02/2017 翻译:陈同学 参考:JVM 调优 — GC 长时间停顿问题及解决方法

本文是 成为Java GC专家 系列的第三篇。

在第一篇 理解 Java GC 中我们学习了不同GC算法的处理过程,GC是如何工作的,什么是年轻代和老年代,JDK7中的5种GC类型,以及每种GC类型对性能的影响。

在第二篇 如何监控Java GC 中讲述了运行中的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 中提到过,不要去想 "有人在使用一些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

  1. 根据分析结果决定是否需要GC优化

在检查GC状态后,你需要分析监控结果,再决定是否进行GC优化。如果分析结果显示GC的时间只不过是0.1-0.3s,就不要浪费时间搞什么GC优化了。然而,如果GC时间有1-3s,甚至超过10s,那GC势在必行

但是,如果你已经分配了10G Java内存,而且没有办法降低内存大小的话,就没办法进行GC优化了。在GC优化之前,你需要思考下为什么需要分配这么大的内存。如果只是分配了1-2G内存并且发生了OutOfMemoryError,你需要获取heap dump来验证和排查原因。

  1. 设置GC类型和内存大小

如果你已经决定要进行GC优化,同时,如果你拥有多台机器,一定要检查不同GC参数对不同机器的影响。

  1. 分析结果

设置GC参数后让程序运行至少24小时,再分析收集到的数据。幸运的话,你可能立马找到对系统最合适的GC参数,否则你需要分析日志,检查内存分配情况,然后再通过调整GC类型和内存大小来找到最佳参数配置。

  1. 如果结果满意,将参数应用到所有机器并停止优化

在下面的部分,你将看到每一步的具体步骤。

监控GC状态&分析结果

检查Web应用GC状态最好的方式是使用 jstat 命令。下面的例子是GC优化之前状态。

检查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日志

最终结论是,每个系统需要其合适的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来试试效果。

为了配置内存大小你还需要设置NewRatioNewRatio是新生代和老年代的比率。如果 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 获取如下数据:

代码语言:txt
复制
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 检查内存使用情况,结果如下:

关键数据如下:

  • 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的结果:

代码语言:txt
复制
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 参数,下面是结果:

上图是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 得到的结果如下:

代码语言:txt
复制
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秒之内。

基于以上结果,我按照Case6调整了GC参数。但是,这导致每天晚上都发生OutOfMemoryError。在这里很难解释具体的原因,简而言之,批数据处理程序导致了内存泄漏,现在相关的问题已经被解决。

如果GC日志的分析时间很短,然后就将优化结果应用到所有服务器是非常危险的。必须牢记,只有在结合GC日志和应用程序进行分析之后才有可能优化成功。

我们了解了两个关于GC优化的例子,正如我之前提到的,例子中提到的GC参数,可以设置在相同服务器(CPU、操作系统、JDK版本、运行的服务相同)之上。

结论

我并未获取heap dump文件,只是凭借经验进行GC优化,精确地分析内存可以得到更好的优化效果。不过这种分析一般适用于内存使用量相对固定的场景。但是,如果服务严重过载并占用的大量的内存,建议你根据上面的经验进行GC优化。


陈同学的公众号,欢迎关注

本文系外文翻译,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系外文翻译前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 有必要优化GC吗?
  • 影响GC性能的参数
  • GC优化的步骤
    • 监控GC状态&分析结果
      • 设置GC类型和内存大小
        • 设置GC类型
        • 并发模式失败
        • 设置内存大小
      • 分析GC优化结果
      • 优化例子
        • 案例1
          • 案例2
          • 结论
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档