我们在工作中的Java应用服务器中遇到了非常奇怪的内存泄漏:在部署新版本的微服务时,JVM进程内存不足,因此崩溃,导致服务中断。

经过一番研究,这类错误似乎在这个应用服务器中非常常见,尤其是在部署应用程序时不重新启动服务器时。常见的修复方法是在投入生产之前重新启动JVM进程,防止内存不足(但不会导致内存泄漏)。这就是我们选择的短期“修复”。

主要来自C/C++背景,我习惯于内存泄漏和内存分析,我喜欢跟踪这些错误并修复它们。因此,我想深入研究细节,以了解更多关于Java内部机制的信息,并确定这个漏洞发生的确切位置,以确定我们是否可以对此采取措施。

得到内存泄露的证据

记录堆内存

我们怀疑某个特定的库是内存泄漏源,首先要做的是确保它确实是问题所在。为此,我启动了应用服务器的本地实例,并在其上部署了WAR。然后,我使用jmap创建了堆内存的快照dump文件。

jmap -dump:live,file=first.bin <pid>

一旦我有了备份,我已经开始重新部署我的WAR文件7次,没有重新启动服务器。

最后,我用jmap创建了另一个堆快照。

内存分析

一旦有了这两个堆快照,我就使用Eclipse内存分析器MAT来读取dump文件。以下是我发现的:

Size: 85.3 MB Classes: 23.5k Objects: 1.9m Class Loader: 436

消耗了853 MB。我个人认为这是一个可以接受的应用程序。让我们进入第二个GC:

Size: 271.9 MB Classes: 35k Objects: 7.1m Class Loader: 1.4k

我们可以看出有一个明显的问题。7次部署后,内存消耗增加了两倍。某处有明显的内存泄漏。是时候采取行动了。

了解内存泄漏问题所在

既然我确信内存泄漏了,我已经使用jmap来查看内存细节,了解是什么消耗了这么多内存。结果令人惊讶:

371 instances of "*ClassLoader", loaded by "jdk.internal.loader.ClassLoaders$AppClassLoader @ 0x7e021a658" occupy 198,789,800 (??.??%) bytes.

Biggest instances:

* ClassLoader @ 0x7ef531c30 - 27,782,296 (9.74%) bytes.
* ClassLoader @ 0x7ee056470 - 27,781,552 (9.74%) bytes.
* ClassLoader @ 0x7e6658b18 - 27,781,208 (9.74%) bytes.
* ClassLoader @ 0x7ec60ab60 - 27,780,856 (9.74%) bytes.
* ClassLoader @ 0x7ef531cd8 - 27,780,032 (9.74%) bytes.
* ClassLoader @ 0x7ea3074b8 - 27,779,608 (9.74%) bytes.
* ClassLoader @ 0x7e31b53b0 - 27,200,584 (9.54%) byte

如您所见,内存中有很多类装入器。最大的实例是以前部署的实例。它们还没有被GC清理干净,这就解释了内存泄漏的原因: 有些东西使这些实例以及它们包含的所有数据保持了活动状态。

在Java中GC是如何工作的

在搜索内存泄漏的原因之前,了解Java垃圾回收的工作原理非常重要。使用的算法称为标记和扫描。简而言之,它是如何工作的:

在Java中,有一些特殊的对象不能在应用程序运行时被垃圾回收。这些对象称为GC根。例如,actives线程、主类中的静态变量、系统类装入器、系统类等…

因此,算法是这样进行的:它将从GC根开始构建一种树,并尝试通过引用它们的用法来确定每个活动对象的路径。当算法完成时,所有未连接到GC根的对象都将成为垃圾回收的候选对象。下面的模式对此进行了解释:

 

因此,如果我们的类加载器在部署后仍然处于活动状态,这意味着我们的应用程序中的某些东西正在将它“链接”到GC根,从而阻止任何垃圾收集。现在我知道该找什么了。

追踪内存泄漏问题

Eclipse内存分析器有一个非常有用的函数,名为“path to GC roots”,它显示了是什么使特定的类保持活动状态。以下是我发现的:

* ClassLoader @ 0x7ee056470
* * contextClassLoader io.github.classgraph.ScanResult$1
* * * [...]
* * * * hooks java.lang.ApplicationShutdownHooks @ 0x7e00863b8 (System class)

如你所见,可疑库在内部使用类图库在类装入器上执行一些操作。这个 ClassGraph 库在 ApplicationShutdownHooks 类(这是一个系统类,因此是一个GC根)上注册了一个 shutdownhook 。 ApplicationShutdownHooks 用于注册在JVM关闭时要执行的特殊代码,由于我们的JVM在我们的情况下没有重新启动(请记住,我们是在不重新启动的情况下进行部署的),所以钩子永远不会被调用,因此仍然是活动的,保持对 ScanResult 对象的引用,防止它成为GC,从而防止我们的整个类加载器也成为它。 我们找到凶手了!

希望ClassGraph是开源的,所以我查找了报告的问题,发现了一些有趣的东西。

已经有人报告了这个bug,它在4.8.51版本中得到了解决。但是这个bug仍然存在,我已经查看了可疑的库源代码,你猜怎么着?他们使用的是4.6.32。bug还在那里。

 

原文链接:http://javakk.com/1132.html

如果觉得本文对你有帮助,可以关注一下我公众号,回复关键字【面试】即可得到一份Java核心知识点整理与一份面试大礼包!另有更多技术干货文章以及相关资料共享,大家一起学习进步!