java.lang.OutOfMemoryError 有 9 种类型,每种类型都表示 Java 应用程序中与内存相关的独特问题。其中,有一个难以诊断的错误。在这篇文章中,我们将深入探讨此错误背后的根本原因,探索潜在的解决方案,并讨论解决此问题的有效诊断方法。让我们用知识和工具武装自己,以征服这个共同的对手。
java.lang.OutOfMemoryError: Metaspace
JVM 内存区域
为了更好地理解 ,我们首先需要了解不同的 JVM 内存区域。这是一个视频剪辑,它很好地介绍了不同的 JVM 内存区域。但简而言之,JVM 具有以下内存区域:OutOfMemoryError
图 1:JVM 内存区域
- 年轻一代: 新创建的应用程序对象存储在此区域中。
- 老一代: 生存期较长的应用程序对象将从 Young 代提升到 Old 代。基本上,此区域包含生存期较长的对象。
- 元空间: 执行程序所需的类定义、方法定义和其他元数据存储在 Metaspace 区域中。此区域是在 Java 8 中添加的。在此之前,元数据定义存储在 PermGen.从 Java 8 开始,PermGen 被 Metaspace 取代。
- 线程: 每个应用程序线程都需要一个线程堆栈。为线程堆栈分配的空间(包含方法调用信息和局部变量)存储在此区域中。
- 代码缓存: 存储方法的已编译本机代码(机器代码)以实现高效执行的内存区域存储在此区域中。
- 直接缓冲器: 现代框架(即 Spring WebClient)使用 ByteBuffer 对象进行高效的 I/O 操作。它们存储在此区域中。
- GC (垃圾回收): 自动垃圾回收工作所需的内存存储在此区域中。
- JNI(Java 本机接口): 用于与本机库交互的内存和用其他语言编写的代码存储在此区域中。
- 杂项: 有一些区域特定于某些 JVM 实现或配置,例如内部 JVM 结构或保留内存空间,它们被归类为“杂项”区域。
什么是 java.lang.OutOfMemoryError:元空间?
图 2:java.lang.OutOfMemoryError:元空间
对于大量的类定义,在比分配的 Metaspace 内存限制(即)的区域中创建的方法定义,JVM 将抛出 .Metaspace
-XX:MaxMetaspaceSize
java.lang.OutOfMemoryError: Metaspace
是什么导致 java.lang.OutOfMemoryError: Metaspace?
java.lang.OutOfMemoryError: Metaspace
在以下情况下由 JVM 触发:
- 创建大量动态类:如果您的应用程序使用 Groovy 类型的脚本语言或 Java 反射在运行时创建新类
- 加载大量类: 要么您的应用程序本身有很多类,要么它使用了很多 3 个RDParty 库/框架,其中包含很多类。
- 加载大量类加载器: 您的应用程序正在加载大量类加载器。
OutOfMemoryError:Metaspace 的解决方案
以下是修复此错误的潜在解决方案:
- 增加 Metaspace 大小:如果由于加载的类数量增加而出现,则增加 JVM 的元空间大小( 和 )。此解决方案足以修复大多数错误,因为 Metaspace 区域中很少发生内存泄漏。
OutOfMemoryError
-XX:MetaspaceSize
-XX:MaxMetaspaceSize
OutOfMemoryError: Metaspace
- 修复内存泄漏:使用本文中给出的方法分析应用程序中的内存泄漏。确保在不再需要类定义时正确取消引用类定义,以允许对它们进行垃圾回收。
生成 OutOfMemoryError 的示例程序:Metaspace
为了更好地理解 ,让我们尝试模拟它。让我们利用 BuggyApp,一个简单的开源混沌工程项目。BuggyApp 可以生成各种性能问题,例如 Memory Leak、Thread Leak、Deadlock、多线程等。下面是 BuggyApp 项目中的 Java 程序,它在执行时进行模拟。java.lang.OutOfMemoryError: Metaspace
BLOCKED
java.lang.OutOfMemoryError: Metaspace
import java.util.UUID; import javassist.ClassPool;
public class OOMMetaspace { public static void main(String[] args) throws Exception { ClassPool classPool = ClassPool.getDefault(); while (true) { // Keep creating classes dynamically! String className = "com.buggyapp.MetaspaceObject" + UUID.randomUUID(); classPool.makeClass(className).toClass(); } } }
在上面的程序中,该方法包含一个无限循环。在循环中,线程使用开源库 javassist 创建名称以 .此程序生成的类名将如下所示:.当创建如此多的此类动态类时,Metaspace 内存区域将达到其极限,JVM 将抛出 .OOMMetaspace’ class’s ‘main()
while (true)
com.buggyapp.MetaspaceObject
com.buggyapp.MetaspaceObjectb7a02000-ff51-4ef8-9433-3f16b92bba78
java.lang.OutOfMemoryError: Metaspace
如何排查 OutOfMemoryError:Metaspace 问题
要诊断 ,我们需要检查 Metaspace 区域的内容。检查内容后,您可以找出应用程序代码的泄漏区域。这是一篇博客文章,其中描述了检查 Metaspace 区域内容的几种不同方法。您可以选择适合您要求的方法。我最喜欢的选项是:OutOfMemoryError: Metaspace
1. -verbose:类
如果您在 Java 版本 8 或更低版本上运行,则可以使用此选项。在启动期间将选项传递给应用程序时,它将打印加载到内存中的所有类。加载的类将打印在标准错误流中(即,如果您没有将错误流路由到日志文件,则为 console)。-verbose:class
例:
java {app_name} -verbose:class
当我们将标志传递给上述程序时,在控制台中,我们开始看到要打印的以下行:-verbose:class
[Loaded com.buggyapp.MetaspaceObjecta97f62c5-0f71-4702-8521-c312f3668f47 from __JVM_DefineClass__] [Loaded com.buggyapp.MetaspaceObject70967d20-609f-42c4-a2c4-b70b50592198 from __JVM_DefineClass__] [Loaded com.buggyapp.MetaspaceObjectf592a420-7109-42e6-b6cb-bc5635a6024e from __JVM_DefineClass__] [Loaded com.buggyapp.MetaspaceObjectdc7d12ad-21e6-4b17-a303-743c0008df87 from __JVM_DefineClass__] [Loaded com.buggyapp.MetaspaceObject01d175cc-01dd-4619-9d7d-297c561805d5 from __JVM_DefineClass__] [Loaded com.buggyapp.MetaspaceObject5519bef3-d872-426c-9d13-517be79a1a07 from __JVM_DefineClass__] [Loaded com.buggyapp.MetaspaceObject84ad83c5-7cee-467b-a6b8-70b9a43d8761 from __JVM_DefineClass__] [Loaded com.buggyapp.MetaspaceObject35825bf8-ff39-4a00-8287-afeba4bce19e from __JVM_DefineClass__] [Loaded com.buggyapp.MetaspaceObject665c7c09-7ef6-4b66-bc0e-c696527b5810 from __JVM_DefineClass__] [Loaded com.buggyapp.MetaspaceObject793d8aec-f2ee-4df6-9e0f-5ffb9789459d from __JVM_DefineClass__] :
:
这清楚地表明,带有前缀的类被如此频繁地加载到内存中。这是一个很好的线索/提示,可以让您知道应用程序中发生泄漏的位置。com.buggyapp.MetaspaceObject
2. -Xlog:类+加载
如果您在 Java 版本 9 或更高版本上运行,则可以使用此选项。在启动期间将选项传递给应用程序时,它将打印加载到内存中的所有类。加载的类将打印在您配置的文件路径中。-Xlog:class+load
例:
java {app_name} -Xlog:class+load=info:/opt/log/loadedClasses.txt
如果您仍然无法根据类名确定泄漏的来源,则可以通过从应用程序获取堆转储来进行深入研究。您可以使用本文中讨论的 8 个选项之一捕获堆转储。 您可以选择适合您需求的选项。捕获堆转储后,您需要使用 HeapHero、JHat 等工具来分析转储。
什么是堆转储?
Heap Dump 基本上是应用程序内存的快照。它包含有关内存中存在的对象和数据结构的详细信息。它将告诉内存中存在哪些对象、它们引用谁、谁在引用、其中存储的实际客户数据是什么、它们占用的大小、它们是否有资格进行垃圾回收等。它们提供了有关应用程序内存使用模式的宝贵见解,帮助开发人员识别和解决与内存相关的问题。
如何通过 Heap Dump 分析 Metaspace 内存泄漏
HeapHero 有两种模式:
- 云:您可以将转储上传到 HeapHero 云并查看结果。
- 本地:您可以在此处注册,在本地计算机上安装 HeapHero,然后进行分析。
注意:我更喜欢使用该工具的本地安装,而不是使用云版本,因为堆转储往往包含敏感信息(例如 SSN、信用卡号、增值税等),并且我不希望在外部位置分析转储。
捕获堆转储后,我们从上述程序中将其上传到 HeapHero 工具。该工具分析了转储并生成了报告。在报告中,转到 'Histogram' 视图。此视图将显示加载到内存中的所有类。在此视图中,您会注意到带有前缀 的类。右键单击类名称旁边的 。然后单击下图所示。com.buggyapp.MetaspaceObject
…
List Object(s) with > incoming references
图 3:显示内存中所有已加载类的直方图视图
完成后,该工具将显示此特定类的所有传入引用。这将显示这些类的原点,如下图所示。它将清楚地显示代码的哪一部分正在创建这些类定义。一旦我们知道代码的哪一部分正在创建这些类定义,那么解决问题就很容易了。