消耗了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,从而防止我们的整个类加载器也成为它。 我们找到凶手了!
最后
2020年在匆匆忙忙慌慌乱乱中就这么度过了,我们迎来了新一年,互联网的发展如此之快,技术日新月异,更新迭代成为了这个时代的代名词,坚持下来的技术体系会越来越健壮,JVM作为如今是跳槽大厂必备的技能,如果你还没掌握,更别提之后更新的新技术了。
更多JVM面试整理:
没掌握,更别提之后更新的新技术了。
[外链图片转存中…(img-RBOnxu5Q-1725075405369)]
更多JVM面试整理:
[外链图片转存中…(img-rqNpKuIX-1725075405370)]