JVM GC如何优化?

GC优化是必要的吗?

或者更准确地说,GC优化对Java基础服务来说是必要的吗?答案是否定的,事实上GC优化对Java基础服务来说在有些场合是可以省去的,但前提是这些正在运行的Java系统,必须包含以下参数或行为:

内存大小已经通过**-Xms 和 -Xmx参数指定过 运行在server模式下(使用-server**参数) 系统中没有残留超时日志之类的错误日志 换句话说,如果你在运行时没有手动设置内存大小并且打印出了过多的超时日志,那你就需要对系统进行GC优化。

不过你需要时刻谨记一句话: GC tuning is the last task to be done.

现在来想一想GC优化的最根本原因,垃圾收集器的工作就是清除Java创建的对象,垃圾收集器需要清理的对象数量以及要执行的GC数量均取决于已创建的对象数量。因此,为了使你的系统在GC上表现良好,首先需要减少创建对象的数量。

俗话说“冰冻三尺非一日之寒”,我们在编码时要首先要把下面这些小细节做好,否则一些琐碎的不良代码累积起来将让GC的工作变得繁重而难于管理:

  • 使用StringBuilder或StringBuffer来代替String
  • 尽量少输出日志

尽管如此,仍然会有我们束手无策的情况。XML和JSON解析过程往往占用了最多的内存,即使我们已经尽可能地少用String、少输出日志,仍然会有大量的临时内存(大约10-100MB)被用来解析XML或JSON文件,但我们又很难弃用XML和JSON。在此,你只需要知道这一过程会占据大量内存即可。

如果在经过几次重复的优化后应用程序的内存用量情况有所改善,那么久可以启动GC优化了。

总结了GC优化的两个目的:

  • 将进入老年代的对象数量降到最低
  • 减少Full GC的执行时间

将进入老年代的对象数量降到最低

除了可以在JDK 7及更高版本中使用的G1收集器以外,其他分代GC都是由Oracle JVM提供的。关于分代GC,就是对象在Eden区被创建,随后被转移到Survivor区,在此之后剩余的对象会被转入老年代。也有一些对象由于占用内存过大,在Eden区被创建后会直接被传入老年代。老年代GC相对来说会比新生代GC更耗时,因此,减少进入老年代的对象数量可以显著降低Full GC的频率。你可能会以为减少进入老年代的对象数量意味着把它们留在新生代,事实正好相反,新生代内存的大小是可以调节的。

降低Full GC的时间

Full GC的执行时间比Minor GC要长很多,因此,如果在Full GC上花费过多的时间(超过1s),将可能出现超时错误。

  • 如果 通过减小老年代内存来减少Full GC时间 ,可能会引起OutOfMemoryError或者导致Full GC的频率升高。
  • 另外,如果 通过增加老年代内存来降低Full GC的频率 ,Full GC的时间可能因此增加。

因此, 你需要把老年代的大小设置成一个“合适”的值 。

影响GC性能的参数

不要幻想着“如果有人用他设置的GC参数获取了不错的性能,我们为什么不复制他的参数设置呢?”,因为对于不用的Web服务,它们创建的对象大小和生命周期都不相同。

举一个简单的例子,如果一个任务的执行条件是A,B,C,D和E,另一个完全相同的任务执行条件只有A和B,那么哪一个任务执行速度更快呢?作为常识来讲,答案很明显是后者。

Java GC参数的设置也是这个道理,设置好几个参数并不会提升GC执行的速度,反而会使它变得更慢。 GC优化的基本原则 是将不同的GC参数应用到两个及以上的服务器上然后比较它们的性能,然后将那些被证明可以提高性能或减少GC执行时间的参数应用于最终的工作服务器上。

下面这张表展示了与内存大小相关且会影响GC性能的GC参数

表1:GC优化需要考虑的JVM参数

 

 

笔者在进行GC优化时最常用的参数是-Xms,-Xmx和-XX:NewRatio。-Xms和-Xmx参数通常是必须的,所以NewRatio的值将对GC性能产生重要的影响。

有些人可能会问 如何设置永久代内存大小 ,你可以用-XX:PermSize和-XX:MaxPermSize参数来进行设置,但是要记住,只有当出现OutOfMemoryError错误时你才需要去设置永久代内存。

还有一个会影响GC性能的因素是垃圾收集器的类型,下表展示了关于GC类型的可选参数(基于JDK 6.0):

 

除了G1收集器外,可以通过设置上表中每种类型第一行的参数来切换GC类型,最常见的非侵入式GC就是Serial GC,它针对客户端系统进行了特别的优化。

会影响GC性能的参数还有很多,但是上述的参数会带来最显著的效果,请切记,设置太多的参数并不一定会提升GC的性能。

GC优化的过程

GC优化的过程和大多数常见的提升性能的过程相似,下面是笔者使用的流程:

1. 监控GC状态

你需要监控GC从而检查系统中运行的GC的各种状态

2. 分析监控结果后决定是否需要优化GC

在检查GC状态后,你需要分析监控结构并决定是否需要进行GC优化。如果分析结果显示运行GC的时间只有0.1-0.3秒,那么就不需要把时间浪费在GC优化上,但如果运行GC的时间达到1-3秒,甚至大于10秒,那么GC优化将是很有必要的。

但是,如果你已经分配了大约10GB内存给Java,并且这些内存无法省下,那么就无法进行GC优化了。在进行GC优化之前,你需要考虑为什么你需要分配这么大的内存空间,如果你分配了1GB或2GB大小的内存并且出现了OutOfMemoryError,那你就应该执行**堆转储(heap dump)**来消除导致异常的原因。

注意: **堆转储(heap dump)**是一个用来检查Java内存中的对象和数据的内存文件。该文件可以通过执行JDK中的jmap命令来创建。在创建文件的过程中,所有Java程序都将暂停,因此,不要在系统执行过程中创建该文件。

3. 设置GC类型/内存大小

如果你决定要进行GC优化,那么你需要选择一个GC类型并且为它设置内存大小。此时如果你有多个服务器,请如上文提到的那样,在每台机器上设置不同的GC参数并分析它们的区别。

4. 分析结果

在设置完GC参数后就可以开始收集数据,请在收集至少24小时后再进行结果分析。如果你足够幸运,你可能会找到系统的最佳GC参数。如若不然,你还需要分析输出日志并检查分配的内存,然后需要通过不断调整GC类型/内存大小来找到系统的最佳参数。

5. 如果结果令人满意,将参数应用到所有服务器上并结束GC优化

如果GC优化的结果令人满意,就可以将参数应用到所有服务器上,并停止GC优化。

在下面的章节中,你将会看到上述每一步所做的具体工作。

监控GC状态并分析结果

在运行中的Web应用服务器(Web Application Server, WAS)上查看GC状态的最佳方式就是使用jstat命令。前面文章中已经介绍过了jstat命令,所以在本篇文章中我将着重关注数据部分。

下面的例子展示了某个还没有执行GC优化的JVM的状态(虽然它并不是运行服务器)。

 

 

我们先看一下YGC(从应用程序启动到采样时发生 Young GC 的次数)和YGCT(从应用程序启动到采样时 Young GC 所用的时间(秒)),计算YGCT/YGC会得出,平均每次新生代的GC耗时50ms,这是一个很小的数字,通过这个结果可以看出,我们大可不必关注新生代GC对GC性能的影响。

现在来看一下FGC( 从应用程序启动到采样时发生 Full GC 的次数)和FGCT(从应用程序启动到采样时 Full GC 所用的时间(秒)),计算FGCT/FGC会得出,平均每次老年代的GC耗时19.68s。有可能是执行了三次Full GC,每次耗时19.68s,也有可能是有两次只花了1s,另一次花了58s。不管是哪一种情况,GC优化都是很有必要的。

使用jstat命令可以很容易地查看GC状态,但是分析GC的最佳方式是加上-verbosegc参数来生成日志。在之前的文章中笔者已经解释了如何分析这些日志。 HPJMeter 是笔者最喜欢的用于分析-verbosegc生成的日志的工具,它简单易用,使用HPJmeter可以很容易地查看GC执行时间以及GC发生频率。

此外,如果GC执行时间满足下列所有条件,就没有必要进行GC优化了:

  • Minor GC执行非常迅速(50ms以内)
  • Minor GC没有频繁执行(大约10s执行一次)
  • Full GC执行非常迅速(1s以内)
  • Full GC没有频繁执行(大约10min执行一次)

括号中的数字并不是绝对的,它们也随着服务的状态而变化。有些服务可能要求一次Full GC在0.9s以内,而有些则会放得更宽一些。因此,对于不同的服务,需要按照不同的标准考虑是否需要执行GC优化。

检查GC状态时,不能只查看Minor GC和Full GC的时间,还必须要 关注GC执行的次数 。如果新生代空间太小,Minor GC将会非常频繁地执行(有时每秒会执行一次,甚至更多)。此外,传入老年代的对象数目会上升,从而导致Full GC的频率升高。因此,在执行jstat命令时,请使用-gccapacity参数来查看具体占用了多少空间。

设置GC类型/内存大小

设置GC类型

Oracle JVM有5种垃圾收集器,但是在JDK后面的版本中,你只能在Parallel GC,Parallel Compacting GC 、CMS GC、G1和ZGC等之中选择,至于具体选择哪个,则没有具体的原则和规则。

1.如果你的堆大小不是很大(比如 100MB ),选择串行收集器一般是效率最高的。

参数: -XX:+UseSerialGC 。

  1. 如果你的应用运行在单核的机器上,或者你的虚拟机核数只有单核,选择串行收集器依然是合 适的,这时候启用一些并行收集器没有任何收益。

参数: -XX:+UseSerialGC 。

  1. 如果你的应用是“吞吐量”优先的,并且对较长时间的停顿没有什么特别的要求。选择并行收集 器是比较好的。

参数: -XX:+UseParallelGC 。

  1. 如果你的应用对响应时间要求较高,想要较少的停顿。甚至 1 秒的停顿都会引起大量的请求失 败,那么选择 G1 、 ZGC 、 CMS 都是合理的。虽然这些收集器的 GC 停顿通常都比较短,但它 需要一些额外的资源去处理这些工作,通常吞吐量会低一些。

参数: -XX:+UseConcMarkSweepGC 、 -XX:+UseG1GC 、 -XX:+UseZGC 等。

从上面这些出发点来看,我们平常的 Web 服务器,都是对响应性要求非常高的。选择性其实就集 中在 CMS 、 G1 、 ZGC 上。而对于某些定时任务,使用并行收集器,是一个比较好的选择。

设置内存大小

下面展示了内存大小、GC运行次数和GC运行时间之间的关系:

大内存空间

  • 减少了GC的次数
  • 提高了GC的运行时间

小内存空间

  • 增多了GC的次数
  • 降低了GC的运行时间

关于如何设置内存的大小,没有一个标准答案,如果服务器资源充足并且Full GC能在1s内完成,把内存设为10GB也是可以的,但是大部分服务器并不处在这种状态中,当内存设为10GB时,Full GC会耗时10-30s,具体的时间自然与对象的大小有关。

既然如此,**我们该如何设置内存大小呢?**通常我推荐设为500MB,这不是说你要通过-Xms500m和-Xmx500m参数来设置WAS内存。根据GC优化之前的状态,如果Full GC后还剩余300MB的空间,那么把内存设为1GB是一个不错的选择(300MB(默认程序占用)+ 500MB(老年代最小空间)+200MB(空闲内存))。这意味着你需要为老年代设置至少500MB空间,因此如果你有三个运行服务器,可以把它们的内存分别设置为1GB,1.5GB,2GB,然后检查结果。

理论上来说,GC执行速度应该遵循1GB > 1.5GB > 2GB,1GB内存时GC执行速度最快。然而,理论上的1GB内存Full GC消耗1s、2GB内存Full GC消耗2s在现实里是无法保证的,实际的运行时间还依赖于服务器的性能和对象大小。因此,最好的方法是创建尽可能多的测量数据并监控它们。

在设置内存空间大小时,你还需要设置一个参数:NewRatio。NewRatio的值是新生代和老年代空间大小的比例。如果XX:NewRatio=1,则新生代空间:老年代空间=1:1,如果堆内存为1GB,则新生代:老年代=500MB:500MB。如果NewRatio等于2,则新生代:老年代=1:2,因此,NewRatio的值设置得越大,则老年代空间越大,新生代空间越小。

你可能会认为把NewRatio设为1会是最好的选择,然而事实并非如此,根据笔者的经验,当NewRatio设为2或3时,整个GC的状态表现得更好。

**完成GC优化最快地方法是什么?**答案是比较性能测试的结果。为了给每台服务器设置不同的参数并监控它们,最好查看的是一或两天后的数据。当通过性能测试来进行GC优化时,你需要在不同的测试时保证它们有相同的负载和运行环境。然而,即使是专业的性能测试人员,想精确地控制负载也很困难,并且需要大量的时间准备。因此,更加方便容易的方式是直接设置参数来运行,然后等待运行的结果(即使这需要消耗更多的时间)。

分析GC优化的结果

在设置了GC参数和-verbosegc参数后,可以使用tail命令确保日志被正确地生成。如果参数设置得不正确或日志未生成,那你的时间就被白白浪费了。如果日志收集没有问题的话,在收集一或两天数据后再检查结果。最简单的方法是把日志从服务器移到你的本地PC上,然后用 HPJMeter 分析数据。

在分析结果时,请关注下列几点(这个优先级是笔者根据自己的经验拟定的,我认为选取GC参数时应考虑的最重要的因素是Full GC的运行时间。):

  • 单次Full GC运行时间
  • 单次Minor GC运行时间
  • Full GC运行间隔
  • Minor GC运行间隔
  • 整个Full GC的时间
  • 整个Minor GC的运行时间
  • 整个GC的运行时间
  • Full GC的执行次数
  • Minor GC的执行次数

找到最佳的GC参数是件非常幸运的,然而在大多数时候,我们并不会如此幸运,在进行GC优化时一定要小心谨慎,因为当你试图一次完成所有的优化工作时,可能会出现OutOfMemoryError错误

评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Tzk_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值