7

ClassLoader和内存泄漏:一个Java爱情故事

 3 years ago
source link: http://javakk.com/1132.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

我们在工作中的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根的对象都将成为垃圾回收的候选对象。下面的模式对此进行了解释:

yauYZrI.png!mobile

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

追踪内存泄漏问题

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

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

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

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK