8 种 Java - 内存溢出之三 - Permgen space

本文最后更新于:2024年7月25日 下午

3.1 Permgen space 概述

Java 应用只允许使用有限的内存。你的应用的内存大小是在启动的时候指定好的。进一步来说,Java 内存被分成 2 个不同的区域,如下图:

这些区域,包括 perm 区,会在 JVM 启动时设置。如果你没有设置,会使用与平台有关的默认配置.

java.lang.OutOfMemoryError: PermGen Space 消息表示永久代 (Permgen) 内存耗尽.

3.2 原因

要理解 java.lang.OutOfMemoryError: PermGen Space 的原因,我们需要理解这个特殊的内存区域是用来干嘛.

实际上,永久代主要是加载和存储类声明。包括组成类的名字和字段 (fields), 方法的字节码,常量池信息,对象数组和类型数组,以及实时编译 (Just In Time compiler) 优化.

从上边的定义,你可以推断出 PerGen 大小需求取决于加载的类的数量这些类声明的大小. 因此我们可以说,java.lang.OutOfMemoryError: PermGen Space 的主要原因是: 太多类或者太大的类被加载到永久代.

3.3 示例

3.3.1 极简案例

正如之前的描述,永久代的使用量和加载到 JVM 里的类的数量强相关。下列代码就是最直接的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import javassist.ClassPool;

public class MicroGenerator {
public static void main(String[] args) throws Exception {
for (int i = 0;i<100_000_000;i++) {
genetate("eu.plumbr.demo.Generated" + i);
}
}

public static Class generate(String name) throws Exception {
ClassPool pool = ClassPool.getDefault();
return pool.makeClass(name).toClass();
}
}

在本例中,源代码在运行时循环迭代并生成类。类 javassist库对生成的复杂性进行了处理.

上面的代码将持续生成新的类并将其定义加载到永久代中直到该区被占满,并且抛出 java.lang.OutOfMemoryError: PermGen Space

3.3.2 重部署案例

一个更复杂和现实的例子,我们经常会在应用重部署时出现 java.lang.OutOfMemoryError: PermGen Space 错误。当你重新部署一个应用时,您的意图是删除以前的类加载器引用的所有以前加载的类,并将其替换新的类加载器加载新版本的类。

不幸的是,很多第三方库处理不当的资源线程 , JDBC 驱动文件系统句柄使卸载以前使用的类加载器变得不可能。这反过来意味着: 在每次重新部署期间,您的类的前一个版本仍然驻留在 PermGen 中,在每次重新部署时生成数十 MB (甚至更多) 的垃圾。

我们假设一个示例应用通过 JDBC 驱动连接到一个关系型数据库。当应用启动时,初始化代码加载 JDBC 驱动来连接数据库。对应于规范,JDBC 驱动程序将自己注册到 java.sql.DriverManager。这个注册包含存储在一个静态的驱动程序管理 (DriverManager) 字段中的一个驱动实例.

现在,当应用从应用服务器卸载,java.sql.DriverManager 仍然会持有那个引用。最后,我们对驱动类进行了实时引用,而驱动类又引用了用于加载应用程序的 java.lang.Classloader

java.lang.Classloader 的那个实例仍然引用这个应用的所有的类,通常会在 Perm 区里占用数十 MB 内存. 这也意味着:只需几次重新部署就可以填充一个常见大小的 PermGen 并在日志中出现 java.lang.OutOfMemoryError: PermGen Space 错误.

3.4 解决方案

3.4.1 解决初始化时的 OutOfMemoryError

当由于 PermGen 耗尽导致在应用运行时出现 OutOfMemoryError 错误,解决方案很简单。应用只需要更多的空间来加载所有类到 Perm 区,因此我们只需要增加它的大小。要这么做,调整应用启动配置,并添加 (如果有就增加)-XX:MaxPermSize 参数如下:

java -XX:MaxPermSize=512m com.yourcompany.YourClass

上述配置会告诉 JVM, Perm 区在开始报 OutOfMemoryError 之前允许增大到 512MB.

3.4.2 解决重部署时的 OutOfMemoryError

当 OutOfMemoryError 就发生在你重新部署应用之后的时候,你的应用有类加载器泄漏的问题。这时,你应该做 heap dump 分析 - 在重部署后做这个 heap dump:

jmap -dump:format=b,file=dump.hprof <process-id>

然后用你最喜欢的 heap dump 分析工具 (Eclipse MAT 是个好工具) 打开。在分析工具中,你可以看重复的类 (duplicate classes), 特别是你自己的应用的。从那里,你需要处理所有的类加载器来找到当前活动的类加载器.

对于非活动的类加载器,您需要通过从非活动类加载器获取最短 GC root 路径来确定阻止它们来进行 gc 的引用。 有了这些信息,你就能定位 root cause. 如果 root cause 是第三方库,你可以通过 Google/StackOverflow 来搜索是否这是一个已知问题来获取 patch 或解决方法。如果是你自己的代码,你需要避免违规引用.

3.4.3 解决运行时的 OutOfMemoryError

当应用在运行时 PermGen 内存溢出,联系我就是最好的方式 (@ ̄ー ̄@).

另一种可选的,不用联系我的方法也是可行的。在这种情况下第一步就是要检查是否 GC 允许卸载来自 PerGen 的这些类。一般 JVM 在这方面是相当保守的 – 类是永生不灭的。所以一旦加载,即使没有代码再使用它们,它们仍然会呆在内存中。这就会变成一个问题:当应用创建了大量的动态类 , 而且生成的这些类是不需要长久存在的。在这种情况下,允许 JVM 卸载类定义会有所帮助。在你的启动脚本种加入以下字段即可实现:
-XX:+CMSClassUnloadingEnabled

默认这是设为 false 的,所以要启用这个,你需要显示地在 Java 选项中设置下列参数。如果你启用了 CMSClassUnloadingEnabled, GC 将也会清理 PermGen, 移除不再需要使用的类。要记住这个参数只在 UseConcMarkSweepGC 也启用的时候才会生效。所以,当使用并行 GC, 或者,我的天呐 – 串行 GC 时,确保你指定你的 GC 策略到 CMS 通过:
-XX:+UseConcMarkSweepGC

如果类可以卸载,问题仍然存在,你应该做 heap dump 分析 – 用类似如下的命令:
jmap -dump:file=dump.hprof,format=b <process-id>

然后用你的最爱的 heap dump 分析工具 (如: Eclipse MAT) 打开这个 dump, 找到加载最多类的类加载器. 从这个类加载器中,您可以继续提取加载的类,并通过实例对这些类进行排序,以获得最大的怀疑项列表。

对于每个可疑项,您需要手工地追溯 root cause 到生成此类类的应用程序代码.

后续会有一篇我通过 Dynatrace 分析某财险公司运行时的 Perm 区 OutOfMemoryError 的案例.

系列文章

  1. 8 种 Java 内存溢出之一:Java Heap Space
  2. 案例 1: 某财险承保系统内存泄漏问题
  3. 8 种 Java - 内存溢出之二 - GC overhead limit exceeded
  4. 案例 2: 某寿险公司核心系统 GC 开销超限问题分析
  5. 8 种 Java - 内存溢出之三 - Permgen space
  6. 案例 3: 某财险公司运行时的 Perm 区内存溢出分析
  7. 8 种 Java - 内存溢出之四 - Metaspace
  8. 8 种 Java - 内存溢出之五 - Unable to create new native thread
  9. 8 种 Java - 内存溢出六 - Out of swap space?
  10. 8 种 Java 内存溢出之七 - Requested array size exceeds VM limit
  11. 8 种 Java 内存溢出之八 - Kill process or sacrifice child

8 种 Java - 内存溢出之三 - Permgen space
https://ewhisper.cn/posts/49764/
作者
东风微鸣
发布于
2017年11月1日
许可协议