详解 java.lang.OutOfMemoryError: Permgen space 错误!

/ Java / 没有评论 / 1455浏览

详解 java.lang.OutOfMemoryError: Permgen space 错误!

PermGen 是一个缩写单词,由 Permanent Generation 组合而成。可翻译为永久代。

所以 java.lang.OutOfMemoryError: PermGen space 错误也就可以理解为:永久代(Permanent Generation) 内存区域已满。

通过前面的两篇文章《为什么会产生 java.lang.OutOfMemoryError: Java heap space 错误以及如何解决?》、《详解 java.lang.OutOfMemoryError: GC overhead limit exceeded 错误!》,我们知道 JVM 会限制 Java 程序的最大内存使用量。而 Java 的堆内存被划分为多个区域,每个区域也都一定的内存使用量。

java8 堆内存分配

主要可分为 3 大类:年轻代、年老代、永久代。

这些区域的最大值,都可以通过 JVM 启动参数 -Xmx 和 -XX:MaxPermSize 指定。如果没有明确指定,则根据操作系统平台和物理内存的大小来确定。

在理解 java.lang.OutOfMemoryError: PermGen space 错误之前,我们先来看看 PermGen 是干什么的?

在 JDK1.7 及之前的版本,永久代(permanent generation) 主要用于存储加载/缓存到内存中的 class 定义,包括 class 的 名称(name),字段(fields),方法(methods)和字节码(method bytecode);以及常量池(constant pool information);对象数组(object arrays)/类型数组(type arrays)所关联的 class,还有 JIT 编译器优化后的 class 信息等。

所以很容易看出:PermGen 的使用量和 JVM 加载到内存中的 class 数量/大小有关。可以说 java.lang.OutOfMemoryError: PermGen space 的主要原因,就是加载到内存中的 class 数量太多或体积太大造成的。

下面我们通过一个演示程序来自己制造一个 java.lang.OutOfMemoryError: PermGen space 错误。

import javassist.ClassPool;
public class MicroGenerator {
  public static void main(String[] args) throws Exception {
    for (int i = 0; i < 100000000; i++) {
      generate("com.xttblog.demo.Generated" + i);
    }
  }
  
  public static Class generate(String name) throws Exception {
    ClassPool pool = ClassPool.getDefault();
    return pool.makeClass(name).toClass();
  }
}

在这个 demo 之前我说到:PermGen 空间的使用量,与 JVM 加载的 class 数量有很大关系。所以我这个 demo 就是靠加载非常多的 class 来产生 java.lang.OutOfMemoryError: PermGen space 错误。

这段代码在 for 循环中, 动态生成了很多 class。这里我借助的是 javassist 工具类。

为了快速看到效果,可以加上适当的 JVM 启动参数,如: -Xmx200M -XX:MaxPermSize=16M 等。

需要注意的是:执行上面这段代码,会生成很多新的 class 并将其加载到内存中,随着生成的 class 越来越多,将会占满 Permgen 空间, 然后抛出 java.lang.OutOfMemoryError: Permgen space 错误。当然,也有可能会抛出其他类型的 OutOfMemoryError。

下面我们一起来看看 3 种不同场景下的 java.lang.OutOfMemoryError: PermGen space 的解决办法!

程序启动时产生 OutOfMemoryError

如果是在程序启动时,PermGen 耗尽而产生 OutOfMemoryError 错误,那应该很容易解决。增加 PermGen 的大小,让程序拥有更多的内存来加载 class 即可。修改 -XX:MaxPermSize 启动参数,类似下面这样:

java -XX:MaxPermSize=512m com.xttblog.XttblogClass

这样配置可以允许 JVM 使用的最大 PermGen 空间为 512MB,如果还不够同样会抛出 OutOfMemoryError。

Tomcat redeploy(重新部署)时产生的 OutOfMemoryError

有时候我们在重新部署 Tomcat web 应用时,很可能会引起 java.lang.OutOfMemoryError: Permgen space 错误。按理说,redeploy 时,Tomcat 之类的容器会使用新的 classloader 来加载新的 class,让垃圾收集器将之前的 classloader (连同加载的class一起)清理掉。

但实际情况可能和你理解的并不一样,很多第三方库,以及某些受限的共享资源。如 thread、JDBC驱动、以及文件系统句柄(handles),都会导致不能彻底卸载之前的 classloader。那么在 redeploy 时,之前的 class 仍然驻留在 PermGen 中,每次重新部署都会产生几十MB,甚至上百MB的垃圾。

假设某个应用在启动时,通过初始化代码加载JDBC驱动连接数据库。根据JDBC规范,驱动会将自身注册到 java.sql.DriverManager,也就是将自身的一个实例(instance) 添加到 DriverManager 中的一个 static 域。

那么,当应用从容器中卸载时,java.sql.DriverManager 依然持有 JDBC 实例(Tomcat经常会发出警告),而JDBC驱动实例又持有 java.lang.Classloader 实例,那么垃圾收集器 也就没办法回收对应的内存空间。

所以生产环境建议不要 redeploy,直接关闭/或 Kill 相关的 JVM,然后从头开始启动即可。

当然也可以进行堆转储分析(heap dump analysis)。在 redeploy 之后,执行堆转储,比如执行下面的命令:

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

然后通过堆转储分析器(如强悍的 Eclipse MAT)加载 dump 得到的文件。找出重复的类,特别是类加载器(classloader)对应的 class。

解决运行时产生的 OutOfMemoryError

如果在运行的过程中发生 OutOfMemoryError,首先需要确认 GC是否能从PermGen中卸载class。官方的JVM在这方面是相当的保守(在加载class之后,就一直让其驻留在内存中,即使这个类不再被使用)。但是,现代的应用程序在运行过程中,会动态创建大量的class,而这些class的生命周期基本上都很短暂,旧版本的 JVM 不能很好地处理这些问题。那么我们就需要允许 JVM 卸载 class。使用下面的启动参数:

-XX:+CMSClassUnloadingEnabled

默认情况下 CMSClassUnloadingEnabled 的值为false,所以需要明确指定。启用以后,GC 将会清理 PermGen,卸载无用的 class。当然,这个选项只有在设置 UseConcMarkSweepGC 时生效。 如果使用了 ParallelGC,或者 Serial GC 时,那么需要切换为CMS:

-XX:+UseConcMarkSweepGC

如果确定 class 可以被卸载,假若还存在 OutOfMemoryError,那就需要进行堆转储分析了。具体的操作,和我上面介绍的命令相类似,我就不再细说了。

参考资料