Xueqiu Engineering Blog

thoughts on creating xueqiu

GC 优化的一些总结

| Comments

合理堆大小的设置

基本上所有会写Java的人也都知道怎么设置堆相关的参数,会设置但并不意味着这个是一个简单事情。常见的OOM问题,大部分都是因为不恰当的堆设置引起的。

该给Java进程配置多大的堆?

换个方式问这个问题: 大堆、小堆对Java进程会有怎样的影响?

  • 对Java程序而言,堆大小最大的影响的在于垃圾回收STW的时间。因此,对在意吞吐量而不在意响应时间的应用,堆可以设置的很大。
  • 而对于响应时间要求比较高的应用,堆大小则需要仔细考虑。设小了,会导致频繁gc,gc效率太低;设大了,则STW的时间太长,无法满足时延要求。

响应时间优先的应用如何确定堆大小?

为方便讨论,做以下假设:

  • 假设响应时间要求99%小于100ms
  • 假设应用qps为1000
  • 平均响应时间为50ms
  • 忽略GC外其他方面对时延造成的影响
  • 假设应用是典型的无状态应用,进程内长生命周期对象主要为连接池、线程池、缓存等

由以上假设,可以做一点简单的计算:

  • 99%的请求100ms以内、每秒1000个请求、平均响应时间50ms,意味着5s内可以留给gc做暂停的时间,最长为100ms。
  • 100ms的暂停时间中,其中有50个请求受gc影响,暂停时间大于50ms,加上业务自身的50ms,超过了100ms;另外50ms,暂停时间小于50ms,因此没有超过100ms. 5s内5000个请求,其中50个超时,刚好99%在100ms以内

通过上面的计算,我们得出一个结论:

在以上假设的条件下,5s内超过可以留给gc做暂停的时间,最长为100ms

这个结论变换为以下说法: 5s内发发生一次young gc,最长时间为100ms, 同时,要求old区gc最长不超过100ms

继续:

  • 有了gc频率和gc最长停止时间,我们就有了推算堆大小的方法
  • 如果young区设置为4G时,刚好能保证5s一次young gc,平均100ms,那么4G的young区是一个能够满足业务目标的young区堆大小设置。

如何确定old区大小?

  • 如果使用并行收集器,那么full gc的时候多半会超过young gc时的最长时间,因为大家算法差不多,但是full gc的时候堆变大了。因此,在响应时间优先的业务中,并行收集器不被推荐,一次full gc就有可能影响整个业务。
  • 如果使用并发收集器(CMS),CMS在old gc时有两次短暂的暂停(initial阶段和remark阶段),能保证这两次暂停小于100ms的整个堆大小设置,是一个可以作为参考值的old区大小设置。

young区大小小于前面4G的合理值会不会更好呢?至少显而易见的是超时率更小了

  • 堆大小会直接影响的一个指标是gc效率,即: gc时间占整个系统运行时间的比例。一般来说堆越大效率会越高,堆越小效率会越底。因此,吞吐量优先的应用,尽可把堆设置的大。

  • gc效率低,说明应用很大部分的cpu时间和等待时间都在gc上,这种开销规模小的时候不明显,但是规模大了以后,需要尽可能减少。

并发收集器(CMS)的问题

通过前面部分可以看到,响应时间优先的应用,并发收集器(CMS)是一个好的选择,但是CMS也有自己的一些问题。这些问题会导致并发收集器长时间暂停,从而影响业务。

相对并行收集器,并发收集器设计为在进行垃圾回收时不会暂停应用,因此并发收集器存在几个根本的问题:

  • 不能暂停进行垃圾回收,意味着进行并发垃圾回收时,需要预留空间,保证垃圾回收进行时,有足够的空间能够装下回收过程中的新增对象
  • 不能暂停,同时CMS本身内存结构的限制,会导致内存碎片。碎片意味着当有大对象出现时,需要进行空间整理。

Concurrent Mode Failure

常见导致这个问题有两种情况:一种确实是内存泄露了,导致无法垃圾回收。这时会看到连续的Concurrent Mode Failure。另一种是预留空间不足,我们主要讨论后一种。

这个错误在CMS的gc日志中有可能会看到,这个问题对应开头第一点,即: 在进行old区垃圾回收的时候,old区空间没了,此时,Java进程会暂停应用,进行full gc

一般来说,增加预留空间就可以解决这个问题,可以选择:

  • 预留空间比例不变的情况下,增加old区堆大小。
  • 堆大小不变的情况下,增加预留比例。即: 缩小-XX:CMSInitiatingOccupancyFraction=70。比如:整个堆4G,young区2G,因此old区有2G。如果设置 -XX:CMSInitiatingOccupancyFraction=70,即表示,old区使用2Gx0.7=1.4G时,触发old区gc,预留空间为600m。如果缩小这个值到-XX:CMSInitiatingOccupancyFraction=50,表示old区使用2Gx0.5=1G时,触发old区gc,预留空间为1g

Promotion Failed

这个问题对应最开始的第二点,碎片问题。

  • 这个问题在young gc时有大对象需要迁移到old区,但是old区没有连续空间,随后会暂停应用,进行空间整理。
  • 因为是由于碎片造成,因此,这个问题出现会比较随机。与业务相关,与业务产生的对象相关。
  • 这个问题基本无解,因为,不进行full gc或者空间整理,那么碎片只会越来越多。
  • 可以考虑增加堆大小,这样能减缓碎片产生,或者在流量小的时候,主动触发一次full gc,减少影响。

碎片问题算是CMS的硬伤,也才有了G1。

以上两个问题,均与业务产生的对象有较大关系,有些业务再大的量也不见得会有上面问题,而有的业务,在很小量的时候就会出现。

CMS参数优化的大致流程

GC问题排查流程

关于G1

G1也是并发收集器,G1解决了CMS碎片的硬伤,同时增加了更多响应时间优先的特性。

  • G1之所以能解决CMS存在的问题,是因为对内存结构进行了彻底的改造。把堆进行了分块处理,可以按照内存块为单位计算其使用率等。
  • G1也使用分代的垃圾回收方式,这点与以前是一样的.
  • G1的优化很多是围绕内存块的优化

G1的垃圾回收log

关于G1的日志可以看Oracle的这篇g1gc logs - basic - how to print and how to understand,写的很详细了。

其中需要注意的是,G1也是并发的时候,与CMS类似分了一些阶段。其中G1收集器在remark阶段和cleanup阶段STW,而CMS在initial阶段和remark阶段STW。

G1涉及到的一些优化参数

  • -XX:G1HeapRegionSize=4m: 设置内存分块的大小。范围是1MB~32MB。是一个很常用的参数。当系统中存在大量大对象的时候,大的Region会提升GC效率。

  • XX:InitiatingHeapOccupancyPercent=50: 设置使用整个对的x%时,系统开始进行并行GC。注意是整个堆的百分比。这与CMS收集器的类似参数不同。

  • -XX:MaxGCPauseMillis=200: 单位为毫秒。此值为建议JVM的最长暂停时间。只是建议值,G1只能尽量保证,而无法完全保证。

  • -XX:ParallelGCThreads: 进行young区和old区垃圾回收时的并发线程数。默认线程数由CPU数量决定(通常小于CPU个数)。当有较大的Update RSet时间时,可以尝试调整此值。

  • -XX:ConcGCThreds: 并发GC时的线程数。mixed GC情况下,较长的cycle start时间,可以尝试调整此值。

  • -XX:+ParallelRefProcEnabled: 当看到有较长的Ref Proc建议配置此值。之前我们从JDK6升级到JDK8时,发现Ref Proc时间有较长的提升,CMS收集器和G1收集器均由这个问题。配置以后暂停明显缩小。

对响应时间优先的应用,G1从目前的使用看情况是比较理想的。即没有CMS存在的硬伤,在优化层面也相对简单。从G1内存结构看,G1实际使用时可以配置比较大的堆。可以预见,G1将会逐步替换目前CMS收集器。

参考文献

1
2
3
我们在招聘 “Java工程师 / 运维开发工程师 / 测试开发工程师” 等职位
详细JD可以参考 http://xueqiu.com/about/jobs
感兴趣的同学欢迎投简历到 wangdong@xueqiu.com

Comments