jvm系列(十):如何优化Java GC「译」

简介:

本文由CrowHawk翻译,地址:如何优化Java GC「译」,是Java GC调优的经典佳作。

Sangmin Lee发表在Cubrid上的"Become a Java GC Expert"系列文章的第三篇《How to Tune Java Garbage Collection》,本文的作者是韩国人,写在JDK 1.8发布之前,虽然有些地方有些许过时,但整体内容还是非常有价值的。译者此前也看到有人翻译了本文,发现其中有许多错漏生硬和语焉不详之处,因此决定自己翻译一份,供大家分享。

本文是“成为Java GC专家”系列文章的第三篇,在系列的第一篇文章《理解Java GC》中,我们了解到了不同GC算法的执行过程、GC的工作原理、新生代和老年代的概念、JDK 7中你需要了解的5种GC类型以及每一种GC对性能的影响。

在系列的第二篇文章《如何监控Java GC》中笔者已经解释了JVM进行实时GC的原理、监控GC的方法以及可以使这一过程更加迅速高效的工具。

在第三篇文章中,笔者将基于实际生产环境中的案例,介绍几个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的工作变得繁重而难于管理:

  • 使用StringBuilderStringBuffer来代替String
  • 尽量少输出日志

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

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

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

  1. 将进入老年代的对象数量降到最低
  2. 减少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性能的参数

正如我在系列的第一篇文章《理解Java GC》末尾提到的,不要幻想着“如果有人用他设置的GC参数获取了不错的性能,我们为什么不复制他的参数设置呢?”,因为对于不用的Web服务,它们创建的对象大小和生命周期都不相同。

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

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

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

表1:GC优化需要考虑的JVM参数
类型 参数 描述
堆内存大小 -Xms 启动JVM时堆内存的大小
-Xmx 堆内存最大限制
新生代空间大小 -XX:NewRatio 新生代和老年代的内存比
-XX:NewSize 新生代内存大小
-XX:SurvivorRatio Eden区和Survivor区的内存比

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

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

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

表2:GC类型可选参数
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
在JDK 6中这两个参数必须配合使用

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

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


GC优化的过程

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

1.监控GC状态

你需要监控GC从而检查系统中运行的GC的各种状态,具体方法请查看系列的第二篇文章《如何监控Java 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程序都将暂停,因此,不要再系统执行过程中创建该文件。

你可以在互联网上搜索heap dump的详细说明。对于韩国读者,可以直接参考我去年发布的书:《The story of troubleshooting for Java developers and system operators》 (Sangmin Lee, Hanbit Media, 2011, 416 pages)

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

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

4.分析结果

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

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

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

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


监控GC状态并分析结果

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

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

$ jstat -gcutil 21719 1s
S0    S1    E    O    P    YGC    YGCT    FGC    FGCT GCT
48.66 0.00 48.10 49.70 77.45 3428 172.623 3 59.050 231.673
48.66 0.00 48.10 49.70 77.45 3428 172.623 3 59.050 231.673

我们先看一下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执行一次)
相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
1月前
|
监控 算法 Java
Java GC调优详解
Java GC调优详解
36 0
|
5月前
|
算法 Java 程序员
JVM-Java的自动垃圾回收机制
JVM-Java的自动垃圾回收机制
|
11月前
|
人工智能 算法 Java
Java里面的GC(垃圾回收机制)
Java里面的GC(垃圾回收机制)
|
消息中间件 算法 Java
98. 我说说你对Java GC机制的理解?(三)
98. 我说说你对Java GC机制的理解?(三)
99 1
98. 我说说你对Java GC机制的理解?(三)
|
存储 算法 Java
98. 我说说你对Java GC机制的理解?(二)
98. 我说说你对Java GC机制的理解?(二)
79 0
98. 我说说你对Java GC机制的理解?(二)
|
存储 算法 Java
98. 我说说你对Java GC机制的理解?一
98. 我说说你对Java GC机制的理解?一
71 1
98. 我说说你对Java GC机制的理解?一
|
存储 监控 算法
JAVA GC垃圾回收机制详解
JAVA GC垃圾回收机制详解
219 0
JAVA GC垃圾回收机制详解
|
存储 监控 算法
Java 垃圾回收(GC)
垃圾回收(Garbage Collection,简写为 GC)可能是虚拟机众多知识点中最为大众所熟知的一个了,也是Java开发者最关注的一块知识点。Java 语言开发者比 C 语言开发者幸福的地方就在于,我们不需要手动释放对象的内存,JVM 中的垃圾回收器(Garbage Collector)会为我们自动回收。但是这种幸福是有代价的:一旦这种自动化机制出错,我们又不得不去深入理解 GC 回收机制,甚至需要对这些"自动化"的技术实施必要的监控和调节。
135 0
Java 垃圾回收(GC)
|
存储 Java C++
Java之GC机制
Java之GC机制
118 0
Java之GC机制
|
存储 算法 Java
Java之GC机制(2)
Java之GC机制(2)
71 0
Java之GC机制(2)