3年前 (2020-11-13)  jvm |   抢沙发  3658 
文章评分 1 次,平均分 5.0

OutOfMemoryErrorMetaspace 背景

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

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

排查JVM元空间metaspace溢出问题

内存泄露分析

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

Java 8之前:

排查JVM元空间metaspace溢出问题

Java8开始:

排查JVM元空间metaspace溢出问题

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

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

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

排查JVM元空间metaspace溢出问题

排查JVM元空间metaspace溢出问题

排查JVM元空间metaspace溢出问题

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

排查JVM元空间metaspace溢出问题

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

排查JVM元空间metaspace溢出问题

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

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

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

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

排查JVM元空间metaspace溢出问题

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

排查JVM元空间metaspace溢出问题

 

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

排查JVM元空间metaspace溢出问题

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

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

排查JVM元空间metaspace溢出问题

实验基于以下想法:

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

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

-javaagent:/lib/org.jacoco/org.jacoco.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内存溢出的方式外,还可以参考这篇文章的排查手段:https://javakk.com/160.html

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/805.html

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册