JAVA GC优化入门
为什么需要优化GC?
JAVA的GC是面试必考的题目,可是在实际项目中什么时候使用GC哪?或者应该什么时候优化GC哪?有句名言:“GC优化永远是最后一项任务”。
在使用GC之前,应该考虑一下进行GC的最根本原因:垃圾收集器需要清除在程序中创建的对象,GC执行的次数即需要被垃圾收集器清理的对象个数,与创建对象的数量成正比,因此,首先应该减少创建对象的数量,我们应该从小事做起,比如需要使用StringBuilder 或者StringBuffer 来替代String;应该尽量少的输出日志;
但是,我们知道有些情况会让我们束手无策,我们眼睁睁的看着XML以及JSON解析占用了大量的内存。即便我们已经尽可能少的使用String以及尽量少的输出日志,但是大量的临时内存仍然被用于XML或者JSON解析,例如10-100MB。但是,舍弃XML和JSON是很难的;或者程序偶尔内存溢出;或者假死影响到了正常的服务,这个时候就可以考虑优化GC了。
那么优化GC可以首相到两个方面:1、一个是将转移到老年代的对象数量降到最少;2、另一个是减少Full GC的执行时间。
将转移到年老代的对象数量降到最少
分代垃圾回收策略是由Oracle JVM提供,不包括可以在JDK7以及更高版本中使用的G1 GC。换句话说,对象被创建在伊甸园空间,而后转化到幸存者空间,最终剩余的对象被送到老年代。某些比较大的对象会在被创建在伊甸园空间后,直接转移到年老代空间。年老代空间上的GC处理会年轻代花费更多的时间。因此,减少被移到年老代对象的数据可以显著地减少Full GC的频率。减少被移到年老代空间的对象的数量,可能会被误解为将对象留在新生代。但是,这是不可能的。取而代之,可以调整新生代空间的大小。
减少Full GC执行时间
Full GC的执行时间比Minor GC要长很多。因此,如果Full GC花费了太多的时间(超过1秒),一些连接的部分可能会发生超时错误。这个时候如果你单纯试图通过减少年老代空间来减少Full GC的执行时间,可能会导致OutOfMemoryError 或者 Full GC执行的次数会增加。与之相反,如果你试图通过增加老年代空间来减少Full GC执行次数,执行时间又会增加。因此,你需要将老年代空间设定为一个“合适”的值。
影响GC性能的参数
不要幻想“某个人设定了GC参数后性能得到极大的提高,我们为什么不和他用一样的参数?”,因为不同的Web服务所创建对象的大小和他们的生命周期都不尽相同。
Java GC参数设定一些参数不但没有提高GC执行速度,反而可能导致他更慢。GC优化的最基本原则是将不同的GC参数用于2台或者多台服务器,并进行对比,并将那些被证明提高了性能或者减少了GC执行时间的参数应用于服务器。下面这个表格列出了GC参数中与内存大小相关的,可以影响性能的参数:
表1:GC优化需要考虑的Java参数
定义 | 参数 | 说明 |
堆内存 | -Xms | 启动JVM时的堆内存空间大小 |
-Xmx | 堆内存的最大值 | |
年轻代 | -XX:NewRatio | 年轻代与年老代的比值 |
-XX:NewSize | 年轻代大小 | |
-XX:SurvivorRatio | 伊甸园空间和幸存者空间的比值 |
我在进行GC优化时经常使用-Xms,-Xmx和-XX:NewRatio。另一个可能影响GC性能的参数是GC类型。下表列出了所有可选的GC类型(基于JDK6.0)。
分类 | 参数 | 备注 |
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 | 在JDK6中这两个参数必须同时使用 |
-XX:+UseG1GC |
GC优化过程
GC优化的过程与大多数性能改善的过程及其类似。下面是使用的GC优化过程:
1.监控GC状态
首先你需要监控GC来检查在系统执行过程中GC的各种状态。
2.在分析监控结果后,决定是否进行GC优化
在检查GC状态的过程中,你应该分析监控结果以便决定是否进行GC优化,如果分析结果表明执行GC的时间只有0.1-0.3秒,那你就没必要浪费时间去进行GC优化。但是,如果GC的执行时间是1-3秒,或者超过10秒,GC将势在必行。
3. 调整GC类型/内存空间
如果你已经决定要进行GC优化,那么就要选择GC类型和设定内存空间。在这时,如果你有几台不同服务器,请时刻牢记,检查每一台服务器的GC参数,并进行有针对性的优化。
4.分析结果
在调整了GC参数并持续收集24小时之后,开始对结果进行分析,如果你幸运的话,你就找到那些最适合系统的GC参数。反之,你需要通过分析日志来检查内存是如何被分配的。然后你需要通过不断的调整GC类型和内存空间大小一边找到最佳的参数。
5. 如果结果令人满意,你可以将该参数应用于所有的服务器,并停止GC优化
有过GC优化结果令人满意,你可以应用于所有的服务器,下面,我们将看到每个步骤的具体任务。
监控GC状态及分析结果
查看运行中的Web Application Server (WAS)的GC状态的最佳方法是通过jstat命令,下面这个例子展现了某个JVM在进行GC优化之前的状态。
如上表,我们先看一下YGC 和YGCT,计算YGCT/YGC,172.623/3428=0.050秒(50毫秒)。这意味着新生代空间上的GC操作平均花费50毫秒。在这种情况,你大可不必担心新生代空间上执行的GC操作。接下来,我们来看一下FGCT 和FGC。计算FGCT/ FGC得到19.68秒,这意味着GC的平均执行时间为19.68秒,可能是每次花费19.68秒执行了三次,也可能是其中的两次执行了1秒而另一次执行了58秒。不论哪种情况,都需要进行GC优化。
通过jstat 命令可以很轻易地查看GC状态,但是,分析GC的最佳方式是通过–verbosegc参数来生成日志。如果GC执行时间满足下面所有的条件,就意味着无需进行GC优化了:
- Minor GC执行的很快(小于50ms)
- Minor GC执行的并不频繁(大概10秒一次)
- Full GC执行的很快(小于1s)
- Full GC执行的并不频繁(10分钟一次)
上面提到的数字并不是绝对的;他们根据服务状态的不同而有所区别,某些服务可能满足于Full GC每次0.9秒的速度,但另一些可能不是。因此,针对不同的服务设定不同的值以决定是否进行GC优化。
在查看GC状态的时候有件事你需要特别注意,那就是不要只关注Minor GC 和Full GC的执行时间。还要关注GC执行的次数,例如,当新生代空间较小时,Minor GC会过于频繁的执行(有时每秒超过1次)。另外,转移到老年代的对象数增多,则会导致Full GC执行次数增多。因此,别忘了加上–gccapacity参数来查看具体占用了多少空间。
设定GC类型/内存空间大小
1.设定GC类型
OracleJVM有5种GC类型,但是在JDK7之前的版本中,只能在Parallel GC, Parallel Compacting GC 和CMS GC之中选择一个,对于选择哪个没有明确的原则和规则,但是有一点是很明确的:CMS GC比Parallel GCs更快。
但是,CMS GC也不总是更快。整体来看,CMS GC模式下的Full GC执行更快,不过,一旦出现并行模式失败,他将比Parallel GC更慢。
CONCURRENT MODE失败
在说明这个问题之前,首先要明白Parallel GC 和 CMS GC 最大的不同来自于压缩任务。压缩任务是通过删除已分配内存空间中的空白空间以便压缩内存,清理内存碎片。在Parallel GC模式下,压缩工作在Full GC执行时进行,这会费很多时间,但是,在执行完Full GC之后,由于能够顺序地分配空间,随后的内存能够被更快的分配。
与之相反的,CMS GC并不进行压缩处理,因此,CMS GC执行的更快。但是,由于没有压缩,在进行磁盘清理之前,内存中会有很多空白空间。这就是说,可能没有足够的空间存储大的对象,例如,虽然老年代空间还有300MB空间,但是一些10MB的对象无法被顺序的存储。在这种情况下,会出现“并行模式失败”警告,并执行压缩处理。在CMS GC模式下,压缩处理的执行时间要比Parallel GCs长很多。这样CMS还不许Parallel GCs效率高。综上所述,你需要找到最适合你的系统的GC类型。每个系统都有最适合他的GC类型等着你去寻找,如果你有6台服务器。我建议你每两台设置相同的参数。并添加 –verbosegc参数,分析结果。
2.设定内存空间大小
下面特别精辟的展示了内存空间大小、GC执行次数和GC执行时间三者间的关系:
- 大内存空间
- 减小GC执行次数
- 增加GC执行时间
- 小内存空间
- 减小GC执行时间
- 增加GC执行次数
关于如何设置内存空间的大小,没有唯一的标准答案。如果服务器资源足够,而且Full GC也可能在1秒内完成,设置为10GB当然可行。但绝大多数服务器并不是这样,当内存设为10GB时,可能要花费10~30秒来执行Full GC。当然,执行时间会随对象的大小而改变。
鉴于如此,我们应该如何设定内存空间大小呢?一般来说,我建议为500MB。不过请注意这不是让你将WAS的内存参数设置为–Xms500m 和–Xmx500m。根据优化GC之前的状态,如果Full GC执行之后内存空间剩余300MB,那么最好将内存设置为1GB(300MB(默认程序占用)+ 500MB(老年代最小空间)+200MB(空闲内存))。也就是说你要为老年代额外设置500MB。因此,如果你有三个执行服务器,内存分别设置为1GB,1.5GB,2GB,并且检查结果。
理论上来讲,GC执行速度应该遵循1GB> 1.5GB> 2GB,因此1GB执行GC速度最快。但是并不说明1GB空间的Full GC会花费1秒而2GB空间会花费2秒。时间取决于服务器的性能和对象的大小。因此,最佳的方式是建立尽可能多的衡量指标来监控他们。
对于内存空间大小,你应该额外设定NewRatio参数。NewRatio参数是新生代和老年代空间的比例,即XX:NewRatio=1意味着新生代与老年代之比为1:1。对于1GB来说就是新生代和老年代各500MB。如果NewRatio为2,意味着新生代老年代之比为1:2,因此该值越大,老年代空间越大,新生代空间越小。
这看似一件不是很重要的事情,但NewRatio参数会显著地影响整个GC的性能。如果新生代空间很小,会用更多的对象被转移到老年代空间,这样导致频繁的Full GC,增加暂停时间。你可以简单的认为NewRatio 为1是最佳的选择,但是,有时可能设置为2或3更好,我就见过很多这样的例子。
如何最快的完成GC优化?对比性能测试的结果应该是最快地方法,为每一台服务器设置不同的参数并监控他们的状态,强烈建议至少监控1或2天的数据。但是,当你对GC优化是,你要确保每次执行相同的负载。并且请求的比率,例如URL都应该是一致的。不过,即便对于专业测试人员要想精确的控制负载也是很难的,并要花费大量的时间准备。因此,相对来说比较方便和容易的方法是调整才参数,之后花费较长的时间收集结果。