头图

Memory Leak Due To Improper Exception Handling

https://dzone.com/articles/me...
译:祝坤荣

本文中,我们会讨论到我们在生产环境遇到的内存问题以及如何解决的。这个应用会在运行几个小时候后无响应。但并不清楚什么导致了应用无响应。

技术栈

这个应用运行在AWS云的规格为r5a.2xlarge的EC2实例。这个应用运行在使用Spring框架的Apache Tomcat服务器。它也用像S3和Elastic Beanstalk这样的AWS服务。应用用了个大heap size(-Xmx):48GB。

定位

我们用yCrash工具来定位这个问题。我们让应用跑15分钟流量。然后在这个应用上执行yCrash脚本。yCrash脚本从应用栈上捕捉了360度数据,分析它们,并展示了问题的根因。yCrash脚本捕捉的数据包括:Garbage Collection日志,线程dump,heap dump,netstat,vmstat,iostat,top和ps。

yCrash分析材料生成了一份内存泄露报告。下面是yCrash生成的heap dump分析报告。

6e75e81d554b7b9b40c35f76bad12f0f.jpeg

图1:大对象报告

能看到yCrash指出“org.apache.logging.log4j.LogManager”是内存中最大的对象。对象占用了总内存的98.2%. 其他对象占用不到2%的内存。以下是这个最大对象的对象树:

058879a15629387c2ce9ba2261108c2c.jpeg

图2:对象引用树

看下对象树中红箭头标的地方。这是应用的起始代码。图2中部分包名被遮盖了防止能看出具体应用。你能看到这个对象包名为“xxxxxxxx.superpower.Main$1.val$hprofParser”占用了98.2%的内存。
应用有个类叫“xxxxxxxxxxxxxxx.Main.”。很明显泄露来自这个Main对象。不过,也看不出"xxxxxxxxxxxxxxx.Main$1”是什么。“$1”指出了这是"xxxxxxxxxxxxxxx.Main”类的第一个匿名内部类。匿名内部类是指你可以在父类中定义一个不用命名的内部类。但这不是一个广泛使用的Java编程实践。不过匿名内部类不但影响了程序的可读性也导致定位困难。

以下是“xxxxxxxxxxxxxxx.Main”的高层概要源码。为了减少噪音和改进可读性,类中的无关代码都被移除了。

23fc23c3d5cb3f9c7a3d6efa7d68256d.jpeg

图3:导致内存泄露的源码

能看到第九行就是匿名内部类。此类继承了PrintingProgressMeter类。PrintingProgressMeter类继承了java.util.Thread。无论任何类继承了java.util.Thread,都会成为一个线程。

在第20行,PrintingProgressMeter线程是被pm.start()方法启动的;在21行,调用了hprofParser.read()的方法;而在22行,用pm.stopReporting()方法停止了线程。这代码看着很正常,对吗?应用的什么可以触发一个内存泄露呢?

问题:异常处理

在21行hprofParser.read()里有特定场景可能会抛异常。如果一个异常抛出,22行的pm.stopReporting()就不会被调用。如果这行代码不被调用,线程就会永远运行不会退出。如果线程不退出,线程和对象的引用(比如hprofParser)不会被回收。它会导致内存泄露。

解决方案

在大多数性能问题里,定位问题的根因很困难。修复它们很简单。
这里就是没有异常。

8983d3409f3ec13570baafd23484757b.jpeg

图4:修复内存泄露的源码

我们将pm.stopReporting()方法移到了finally中。在Java语言中,放在finally代码块中的代码无论会不会抛异常都会执行。finally块的内容可以在这里https://docs.oracle.com/javas...了解下。这样,即使hprofParser.read()方法抛了异常,pm.stopReporting方法仍会被调用,让线程终结。如果线程被终结,在垃圾回收时所有对象的引用就会被回收。

当改动后,问题立刻解决了。


本文来自祝坤荣(时序)的微信公众号「麦芽面包」,公众号id「darkjune_think」

开发者/科幻爱好者/硬核主机玩家/业余翻译
转载请注明。

交流Email: zhukunrong@yeah.net


祝坤荣
1k 声望1.5k 粉丝

科幻影迷,书虫,硬核玩家,译者