排查JVM元空间metaspace溢出问题

微信扫一扫,分享到朋友圈

排查JVM元空间metaspace溢出问题

[收起]
文章目录

OutOfMemoryError
:Metaspace 背景

我最近开始使用 Jython
,以便在Delphix的一个项目的Java虚拟机( JVM
)中执行Python代码。对于那些不熟悉Jython的人来说,它是基于JVM的Python实现。您可以将Python源代码编译为Java字节码并在JVM中执行。当我们开始使用Jython时,一切都很顺利……直到我们开始对我们的产品进行功能测试。每隔一次测试运行都会遇到
java.lang.OutOfMemoryError: Metaspace

元空间错误。继续阅读找出原因。

使用Jython很容易。这就像生成一个 PythonInterpreter
的实例、Jython解释器的Java包装器一样简单,您就可以执行任意Python代码了。在我们的项目中,我们创建了一个沙盒,这样代码就不能执行任何恶意的系统调用。作为沙盒的一部分,我们导入大约50个白名单的模块,客户可以使用。每个模块都被编译成一个类文件。这意味着,对于Jython解释器对象的一个实例,我们必须加载大约50个新的Java类。必须指出的是,在我们的初始设计中,我们有多个Jython解释器运行不同的代码。我们使用单元测试对代码进行了压力测试,该单元测试将并行创建数百个Jython解释器并执行一些Python代码。我们从来没有遇到过任何问题。然而,一旦我们开始对我们的产品进行功能测试,我们就开始
java.lang.OutOfMemoryError: Metaspace

非常频繁。

内存泄露分析

回顾一下, metaspace
是Java进程中包含类元数据的区域。在java8之前, metaspace
位于堆上,但从java8开始,它被移出堆,进入本机内存。默认情况下,元空间仅受JVM进程可用的本机内存量的限制,但实际上您应该将其限制为适合您的应用程序的大小(这需要一些调优和实验)。您可以使用名为 MaxMetaspaceSize
的JVM标志来限制元空间的大小。如果您不限制元空间,您可能直到很晚才注意到内存泄漏(可能是在生产设置中)。

Java 8之前:

Java8开始:

有一些事情可能会导致内存不足的元空间错误。最常见的是:

  • 加载的类太多
  • 加载了重复的类
  • Large classes
  • 类加载器泄漏

当发生元空间错误时,调查的第一步是查看JVM进程生成的堆转储。为了研究堆转储,我一直在使用eclipse MAT
(内存分析器工具)。我首先使用“ 重复类
”特性来查看是否有一些类可能会无正当理由 多次加载

看上面的图片,您立即看到有许多Java类的名称以 $py
结尾。这些class中有很多将近20份!在eclipse MAT中查看线程概述,只有少数线程执行Python代码。这意味着Jython解释器对象不是被垃圾收集器清理干净,就是清理得非常慢。现在让我们看看哪些对象阻止这些类被垃圾收集。

通过合并到 shortest paths to the garbage collector roots
垃圾收集器根的最短路径,我们可以看到阻止这些类被清理的大多数引用都来自系统终结器 Finalizer

提醒一下,所有实现 finalize()
方法的对象在被垃圾回收之前都会排队。有一个后台进程终结器线程正在运行并执行每个对象的 finalize()
方法。只有这样,垃圾回收器才能释放与这些对象关联的内存。

就像 Brian Goetz
在关于“垃圾收集和性能”的文章中指出的:

在回收可终结对象之前,至少需要两个垃圾回收周期(在最佳情况下)。

eclipse MAT有一项功能是“ Finalizer Overview
”来查看队列中等待完成的对象。

当我在Finalizer Overview终结器概述里看到超过 58万
个Jython对象时,大吃一惊(全是org.python*开头的)正在等待被释放 finalization
。你可以看到 PythonTree
PyString
PyStringMap
等正在等待终结器线程。深入到Jython源代码中,注意到这些类都没有实现 finalize()
方法。但它们都从PyObject继承 finalize()
方法。

但是 PyObject
finalize()
方法是空的,它包含了一个注释,它为进一步的研究提供了一些线索。

从注释看出Jython代码期望空的 finalize()
方法被优化掉。在编译期中显然没有这种情况,因为我尝试了一个实验,我用一个空的 finalize()
方法编译了一个Java类,在反编译之后它仍然存在。这意味着空 finalize()
方法必须在运行时由实时( JIT
)编译器优化。

但在JVM中并不是这样,我们启动JVM来运行功能测试。这让我想到,也许我们的JVM flags
标志之一(我们有很多)阻止JIT编译器优化空的 finalize
方法。因此,我决定设计一个小实验来帮助我找到”罪魁祸首”。

实验基于以下想法:

  1. 编译具有空 finalize()
    方法的 EmptyFinalize
    类(在上面的屏幕截图中)的源代码
  2. 启动JVM进程时,除了在测试VM上运行的功能测试中使用的一个标志外,其余的都使用
  3. 创建 EmptyFinalize
    的实例
  4. 进入无限循环
  5. dump堆快照
  6. 验证系统终结器是否在 EmptyFinalize
    对象的垃圾回收器根目录中(空的 finalize()
    方法没有进行优化)
  7. 重复上述步骤,直到第6点

经过漫长而乏味的过程后,我发现了导致JIT编译器无法优化空 finalize()
方法的标志:

-javaagent:/lib/org.<a target="_blank" href="https://www.colabug.com/goto/aHR0cDovL2phdmFray5jb20vdGFnL2phY29jbw==" rel="nofollow" target="_blank">jacoco</a>/org.<a target="_blank" href="https://www.colabug.com/goto/aHR0cDovL2phdmFray5jb20vdGFnL2phY29jbw==" rel="nofollow" target="_blank">jacoco</a>.agent-0.8.5.jar

JaCoCo是用来测量函数和单元测试运行中代码覆盖率的工具。也就是说,上面的flag标志只在测试运行期间传递给我们的JVM进程。 这就解释了为什么我不能在本地复制这个问题!

那为什么JaCoCo会阻止JIT完成它的工作呢?JaCoCo对Java进程做了什么?JaCoCo的文档揭示了这个问题:

覆盖分析机制

覆盖率信息必须在运行时收集。为此, JaCoCo
创建原始类定义的插入指令的版本。插装过程是在使用所谓的Java代理加载类的过程中动态进行的。

字节码操作

检测需要修改和生成Java字节码的机制。 JaCoCo
在内部为此使用了 ASM
库。

当然,为了让JaCoCo测量代码覆盖率,它需要在运行时插入Java字节码。空 finalize()
方法没有得到优化, 因为它们从不为空!
我可能会注意到,如果我使用相同的Java字节码操作库(ASM)来检查Jython对象的字节码,这些对象的 finalize()
方法将被优化掉。

结论及解决方案

代码覆盖工具导致JVM元空间metaspace溢出

在测试中我们将JaCoCo代理传递给JVM进程,而没有指定要检测哪些Java包来测量代码覆盖率。这意味着我们最终将检测项目中 所有依赖项的字节码!

jacoco java agent
允许您传递标志以排除或包含要为其创建代码覆盖率的包。所以说你应该只测量自己代码的代码覆盖率,而不是第三方依赖项,这可以通过传递 include=com.your.package.name.*
标记到JaCoCO代理。

除了使用eclipse的MAT分析metaspace内存溢出的方式外,还可以参考这篇文章的排查手段: http://javakk.com/160.html

微信扫一扫,分享到朋友圈

排查JVM元空间metaspace溢出问题

Python自动化操作PPT看这一篇就够了

上一篇

你也可能喜欢

排查JVM元空间metaspace溢出问题

长按储存图像,分享给朋友