3. Permgen space

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里的类的数量强相关. 下列代码就是最直接的例子:

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), 特别是你自己的应用的. 从那里, 你需要处理所有的类加载器来找到当前活动的类加载器.

具体如何在MAT中看重复的类(duplicate classed)可以参考我的另一篇文章.

对于非活动的类加载器, 您需要通过从非活动类加载器获取最短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的案例.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章