4.1 java.lang.OutOfMemoryError: Metaspace 概述

Java应用只允许使用有限的内存. 你的应用可以用的准确的内存大小在启动的时候指定. 展开来说, Java内存被分成不同的区域, 具体如下图:

java 进程namespace隔离 java中namespace_加载

所有的这些区域, 包括元空间(metaspace)区域, 可以在JVM启动的时候指定. 如果你没有指定这些的大小, 平台相关的默认配置会被应用.

java.lang.OutOfMemoryError: Metaspace 消息表明Metaspace区内存耗尽.

4.2 原因

如果你不是Java领域的新手, 你可能会熟悉Java内存管理的另一个叫做: PermGen的概念. 从Java 8开始, Java的内存模型发生明显改变. 引入一个叫做Metaspace的新的内存区域, Permgen被移除. 这个变更是基于多种原因的, 包括但不限于:

  • PermGen需要的内存大小难以预测. 它导致有可能由于内存不足触发java.lang.OutOfMemoryError: Permgen错误或预留过多导致浪费资源.
  • GC性能的提升, 在没有GC暂停和元数据的特定迭代器的情况下启用并发类数据分配.
  • 支持进一步优化,如G1并发类卸载。

所以, 如果你熟悉PermGen, 那么你需要知道的就是 – Java 8之前版本在PermGen里存在的一切东西(组成类的类的名字和字段, 方法的字节码, 常量池信息, 对象数组和类型数组, 以及实时编译优化) – 限制都在Metaspace里.

如你所见, Metaspace 大小需求取决于加载的类的数量和这些类声明的大小. 所以, 很明显java.lang.OutOfMemoryError: Metaspace的主要原因是: 要不太多类, 要不太大的类被加载到Metaspace中.

4.3 案例

就如我们在之前解释的那样, Metaspace使用率与加载到JVM中的类的数量强相关. 下列代码就是最简单的例子:

public class Metaspace {
    static javassist.ClassPool cp = javassist.ClassPool.getDefault();

    public static void main(String[] args throws Exception {
        for (int i=0; ;i++) {
            Class c = cp.makeClass("eu.plumbr.demo.Generated" + i).toClass();
        }
    }
}

在这个例子中, 源代码循环遍历一个循环并在运行时生成类. 所有这些生成的类定义在持续地消耗Metaspace. javassist库对类生成的复杂性进行了处理.

代码会持续生成新的类, 并加载他们的定义到Metaspace中直到空间被完全占满, java.lang.OutOfMemoryError: Metaspace抛出. 当在Mac OS X, Java 1.8.0_05上使用-XX:MaxMetaspaceSize=64m大概加载70,000个类会死掉.

4.4 解决方案

第一个解决因为Metaspace内存溢出的方案很明显. 如果应用耗尽了Metaspace的内存, 你应该增加Metaspace的大小. 调整应用运行配置, 调整下列参数:
-XX:MaxMetaspaceSize=512m

上述配置案例告诉JVM, Metaspace允许在抛出OutOfMemoryError的错误之前增长到512MB.

另一个解决方案乍一看更容易. 你可以通过删除这个参数移除Metaspace的限制. 但是需要注意的是, 你这么做, 可能会导致swap的大量消耗和/或导致本机物理内存分配失败.

通过使用上述建议的 “快速修复”, 你只会通过隐藏 java.lang.OutOfMemoryError: Metaspace掩盖症状, 而不是从根本上解决问题.

如果应用程序内存泄漏或是加载的不合理的东西到Metaspace, 上述解决方案实际上不会改善任何事情, 它只会推迟问题。