设为首页 加入收藏

TOP

一次Java内存泄漏调试的有趣经历(二)
2018-08-31 18:27:08 】 浏览:483
Tags:一次 Java 内存 泄漏 调试 有趣 经历

因此,虽然从纯技术的角度来说,这个问题如此长时间没解决确实很丢人,然而从战略性的角度来看,或许留着这个浪费内存的问题不管,是更务实的选择。当然,另一个考虑就是这个问题一旦发生,会造成什么影响。我们几乎没有对用户造成任何影响,不过结果有可能更糟糕。软件工程就是权衡利弊,决定不同任务的优先级也不例外。

还是不行

有了更多使用 RX 的经验之后,我们可以很简单的解决 ComplerableFurue 的问题。重写代码,只使用 RX;在重写的过程中,升级到 RX2;真正的流式处理数据,而不是在内存里收集它们。这些改动通过 code review 之后,部署到开发环境进行测试。让我们吃惊的是,应用所需的内存丝毫没有减少。内存抽样显示,相较之前,内存中广告对象的数量有所减少。而且对象的数量现在不会一直增长,有时也会下降,因此他们不是全部在内存里收集的。还是老问题,看起来这些数据仍然没有真正的被归集成流。

那现在是怎么回事?

相关的关键词刚才已经提到了:背压。当数据被流式处理,生产者和消费者的速度不同是很正常的。如果生产者比消费者快,并且不能把速度降下来,它就会一直生产越来越多的数据,消费者无法以同样的速度处理掉他们。现象就是未处理数据的缓存不断增长,而这就是我们应用中真正发生的。背压就是一套机制,它允许一个较慢的消费者告诉较快的生产者去降速。

我们的索引系统没有背压的概念,这在之前没什么问题,反正我们把整个索引都保存到内存里了。一旦我们解决了之前的问题,开始真正的流式处理数据,缺少背压的问题就变得很明显了。

这个模式我在解决性能问题时见过很多次了:解决一个问题时会浮现另一个你甚至没有听说过的问题,因为其他问题把它隐藏起来了。如果你的房子经常被淹,你不会注意到它有火灾隐患。

修复由修复引起的问题

在 RxJava 2 里,原来的 Observable 类被拆成了不支持背压的 Observable 和支持背压的 Flowable。幸运的是,有一些简单的办法,可以开箱即用的把不支持背压的 Observable 改造成支持背压的 Flowable。其中包含从非响应式的资源比如 Iterable 创建 Flowable。把这些 Flowable 融合起来可以生成同样支持背压的 Flowable,因此只要快速解决一个点,整个系统就有了背压的支持。

有了这个改动之后,我们把堆从 12 GB 减少到了 3 GB ,同时让系统保持和之前同样的速度。我们仍然每隔数小时就会有一次暂停长达 2 秒的 full GC,不过这比我们之前见到的 20 秒的暂停(还有系统崩溃)要好多了。

再次优化 GC

但是,故事到此还没有结束。检查 GC 的日志,我们注意到大量的过早提升,占到 70%。尽管性能已经可以接受了,我们也尝试去解决这个问题,希望也许可以同时解决 full GC 的问题。

如果一个对象的生命周期很短,但是它仍然晋升到了老年代,我们就把这种现象叫做过早提升(premature tenuring)(或者叫过早升级)。老年代里的对象通常都比较大,使用与新生代不同的 GC 算法,而这些过早提升的对象占据了老年代的空间,所以它们会影响 GC 的性能。因此,我们想竭力避免过早提升。

我们的应用在索引的过程中会产生大量短生命周期的对象,因此一些过早提升是正常的,但是不应该如此严重。当应用产生大量短生命周期的对象时,能想到的第一件事就是简单的增加新生代的空间。默认情况下,G1 的 GC 可以自动的调整新生代的空间,允许新生代使用堆内存的 5% 至 60%。我注意到运行的应用里,新生代和老年代的比例一直在一个很宽的幅度里变化,不过我依然动手修改了两个参数:-XX:G1NewSizePercent=40 和 -XX:G1MaxNewSizePercent=90看看会发生什么。这没起作用,甚至让事情变得更糟糕了,应用一启动就触发了 full GC。我也尝试了其他的比例,不过最好的情况就是只增加 G1MaxNewSizePercent而不修改最小值。这起了作用,大概和默认值的表现差不多,也没有变好。

尝试了很多办法后,也没有取得什么成就,我就放弃了,然后给 Kirk Pepperdine 发了封邮件。他是位很知名的 Java 性能专家,我碰巧在 Allegro 举办的 Devoxx 会议的训练课程里认识了他。通过查看 GC 的日志以及几封邮件的交流,Kirk 建议试试设置 -XX:G1MixedGCLiveThresholdPercent=100。这个设置应该会强制 G1 GC 在 mixed GC 时不去考虑它们被填充了多少,而是强制清理所有的老年代,因此也同时清理了从新生代过早提升的对象。这应该会阻止老年代被填满从而产生一次 full GC。然而,在运行一段时间以后,我们再次惊讶的发现了一次 full GC。Kirk 推断说他在其他应用里也见到过这种情况,它是 G1 GC 的一个 bug:mixed GC 显然没有清理所有的垃圾,让它们一直堆积直到产生 full GC。他说他已经把这个问题通知了 Oracle,不过他们坚称我们观察到的这个现象不是一个 bug,而是正常的。

结论

我们最后做的就是把应用的内存调大了一点点(从 3 GB 到 4 GB),然后 full GC 就消失了。我们仍然观察到大量的过早提升,不过既然性能是没问题的,我们就不在乎这些了。一个我们可以尝试的选项是转换到 GMS(Concurrent Mark Sweep)GC,不过由于它已经被废弃了,我们还是尽量不去使用它。

那么这个故事的寓意是什么呢?首先,性能问题很容易让你误入歧途。一开始看起来是 ZooKeeper 或者 网络的问题,最后发现是我们代码的问题。即使意识到了这一点,我首先采取的措施也没有考虑周全。为了防止 full GC,我在检查到底发生了什么之前就开始调优 GC。这是一个常见的陷阱,因此记住:即使你有一个直觉去做什么,先检查一下到底发生了什么,再检查一遍,防止浪费时间去错误的问题。

第二条,性能问题太难解决了。我们的代码有良好的测试覆盖率,而且运行的特别好,但是它也没有满足性能的要求,它在开始的时候就没有清晰的定义好。性能问题直到部署之后很久才浮现出来。由于通常很难真实的再现你的生产环境,你经常被迫在生产环境测试性能,即使那听起来非常糟糕。

第三条,解决一个问题有可能引发另一个潜在问题的浮现,强迫你不断挖的比你预想的更深。我们没有背压的事实足以中断这个系统,但是直到我们解决了内存泄漏的问题后,它才浮现。

我希望我们这个有趣的经历,能在你解决自己遇到的性能问题时发挥一些作用。

原文链接: allegro.tech 翻译: ImportNew.com - yizhe
译文链接: http://www.importnew.com/29591.html
[ 转载请保留原文出处、译者和译文链接。]

关于作者: yizhe

(新浪微博:@今天我行吗

查看yizhe的更多文章 >>

首页 上一页 1 2 下一页 尾页 2/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇Map大家族的那点事儿(2) :Abstra.. 下一篇Java异常处理的9个最佳实践

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目