G1收集器中的tospace-exhausted问题一则

43次阅读

共计 2006 个字符,预计需要花费 6 分钟才能阅读完成。

最近刚刚将自己的一个应用从 CMS 升级到 G1,在一天早上,刚刚到办公室坐下,就收到手机一阵报警,去查看了监控,发现机器的内存出现了一个 90 度的涨幅,如下图所示:

在查看 GC 日志后,发现那个时间点附近出现了“to-space exhausted”这种日志(关于 G1 的日志学习,参见我之前的文章:【译】深入理解 G1 的 GC 日志(一)
))

在这里,我比较奇怪的是为啥 to-sapce exhausted 会导致整个机器的内存激增。我们 JVM 团队同学给我的解释是:老区不够了,这个时候会把 young 区所有对象不管死活都转成 old 区对象,所以总的内存使用量会暴增。这一个知识点,我之前学习 G1 的时候还真没有 get 到(关于 G1 的基本知识,参见之前的文章:可能是最全面的 G1 学习笔记
))。

不过,我有另外一个疑问:xmx 和 xms 相同的话堆空间应该不变,一开始就分配 5g,然后加上非堆内存,那么 java 进程起来后就会超过 5g,这是没问题的;但是这里利用空闲的内存也应该是利用堆上的空间,然后整体的内存块应该已经分配出去了,应该不会出现机器内存激增的情况。JVM 团队的同学给我解释道:没有,第一次读写到了才会实际从 os 分配出来物理内存。

针对上面的问题,我们最终确定了下面的调优建议:

  • 这次没有发生 FGC,可能是由于我前面将 xmx 和 xms 调大了导致的,这次准备将 xmx 和 xms 先调回到原来的值;
  • 加上 HeapDumpAfterFullGC 参数,下次再发生类似情况的时候,就会触发 FGC,然后自动 dump 堆内存,就可以针对堆内存进行分析,看看是什么对象占用了这么多内存,然后就可以针对性优化。

关于 to-space exhausted 的更多总结

基于上面这个问题,我又去找了一些资料,整理如下。

《Java 性能权威指南》

在这本书的 123 页有提到,上面这种情况属于晋升失败的情况——G1 收集器完成了标记阶段,开始启动混合式垃圾收集,准备要清理老年代分区,但是老年代分区在垃圾收集器释放出足够的空间之前就已经被耗尽了。这种失败通常意味着混合式垃圾收集需要更迅速得完成垃圾收集,每次新生代垃圾收集需要处理更多的老年代分区。一般来说,一系列的 to-space exhausted 之后会跟着一次 FGC。

在我们上面的这个例子中,是 old 区的使用速度超过了垃圾收集器的回收速度,因此可以考虑两种调优的思路

  • 让 G1 更早得启动混合式垃圾收集周期,通过调小 -XX:InitiatingHeapOccupancyPercent=N 这个参数,默认情况下该参数是 45(PS:这个参数表示的是占用整个堆内存的比例),不过,这个参数也不能调得太小,否则会导致过多的并发收集周期和混合式垃圾收集,给应用早成过多的停顿。
  • 除了考虑增加速度,还可以考虑增加每次混合式垃圾收集收集的 Old 分区数量,通过调整 -XX:G1MixedGCCountTarget=N 参数可以控制每个混合式周期中回收的 Old 分区数量,该参数的默认值是 8;

《Java 性能调优指南》

要特别关注日志片段中的 ”to-space exhausted” 和“Evacuation Failure”两个日志,如下图所示。可以看出,Evacuation Failure 消耗了 684.1ms,也就是说,这次转移失败导致了将近 1s 的应用暂停。

这种情况属于转移失败,这本书给出了两点建议:

  • 和《Java 性能权威指南》一样,也建议调小 -XX:InitiatingHeapOccupancyPercent=N 这个参数的值,因为转移失败的代价比多执行一些并发标记周期高很多
  • 建议通过调整-XX:ConcGCThreads,增加用于垃圾收集的线程个数,代价是会多一些 CPU 的消耗;也就是会占用 Java 应用的 CPU 时间,这一点也需要权衡一下。
  • 有时候转移失败是由于 survivor 分区中没有足够的空间容纳新晋升的对象,如果是这种情况,还可以考虑增加 -XX:G1ReservePercent 的大小,在 G1 中这个默认值是 10%。

总结

JVM 参数的调优,是一个不断推导和尝试的过程,其中最重要的数据就是 GC 日志和 Java 堆内存快照,因此:(1)在 JVM 参数中一定要设置 HeapDumpAfterFullGC 和 HeapDumpOnOutOfMemoryError 两个参数,可以在发送 FGC 和 OOM 的时候将当时的 Java 堆情况记录下来,用于事后分析;(2)GC 日志要单独打印到一个日志文件中,方便分析,如果不特别设置,GC 日志会打印到 stdout.log 中,会有其他的日志混合在中间,影响问题排查。

JVM 的参数调优并不是万能的,发生 OOM 或者 FGC 的时候,业务代码中也一定有不合理的地方,需要做合理的限制和优化,不能将所有的事情都交给 JVM 抗。


本号专注于后端技术、JVM 问题排查和优化、Java 面试题、个人成长和自我管理等主题,为读者提供一线开发者的工作和成长经验,期待你能在这里有所收获。

正文完
 0