本文来自于HeapDump性能社区! !有性能问题,上HeapDump性能社区!
正文:
Java 应用程序只允许使用有限的内存量。您的特定应用程序可以使用的确切内存量是在应用程序启动期间指定的。为了让事情变得更复杂,Java 内存被分成不同的区域,如下图所示:
所有这些区域的大小,包括 permgen 区域,都是在 JVM 启动期间设置的。如果您不自己设置大小,将使用特定于平台的默认值。
该java.lang.OutOfMemoryError:PermGen space的消息表明永久代的内存区域被耗尽。
1,什么原因造成的?
要了解java.lang.OutOfMemoryError: PermGen space 的原因,我们需要了解此特定内存区域的用途。
出于实际目的,永久代主要由加载并存储到 PermGen 中的类声明组成。这包括类的名称和字段、带有方法字节码的方法、常量池信息、与类关联的对象数组和类型数组以及即时编译器优化。
从上面的定义中,您可以推断出永久代的大小要求取决于加载的类的数量以及此类声明的大小。因此,我们可以说*java.lang.OutOfMemoryError: PermGen space 的*****主要原因是加载到永久代的类太多或类太大。
2,举个例子
如上所述,永久代空间的使用与加载到 JVM 中的类数量密切相关。下面的代码是最直接
导入 javassist.ClassPool;
public class MicroGenerator {
public static void main(String[] args) 抛出异常 {
for (int i = 0; i < 100_000_000; i++) {
generate("eu.plumbr.demo.Generated" + i);
}
}
public static Class generate(String name) throws Exception {
ClassPool pool = ClassPool.getDefault();
返回 pool.makeClass(name).toClass();
}
}
在这个例子中,源代码遍历一个循环并在运行时生成类。javassist库负责处理类生成的复杂性。
启动上面的代码将继续生成新类并将它们的定义加载到永久空间中,直到空间被完全利用并抛出java.lang.OutOfMemoryError: Permgen 空间。
重新部署时间示例
对于更复杂和更现实的示例,让我们带您了解在应用程序重新部署期间发生的java.lang.OutOfMemoryError: Permgen space错误。当您重新部署应用程序时,您会期望垃圾回收将摆脱引用所有先前加载的类的先前类加载器,并将其替换为加载类的新版本的类加载器。
不幸的是,许多 3rd 方库和对资源(例如线程、JDBC 驱动程序或文件系统句柄)的处理不当使得无法卸载以前使用的类加载器。这反过来意味着在每次重新部署期间,您的类的所有先前版本仍将驻留在 PermGen 中,在每次重新部署期间生成数十兆字节的垃圾。
让我们想象一个使用 JDBC 驱动程序连接到关系数据库的示例应用程序。当应用程序启动时,初始化代码加载 JDBC 驱动程序以连接到数据库。对应于规范,JDBC 驱动程序使用java.sql.DriverManager注册自己。此注册包括在DriverManager的静态字段中存储对驱动程序实例的引用。
现在,当应用程序从应用程序服务器中卸载时,java.sql.DriverManager仍将保留该引用。我们最终获得了对驱动程序类的实时引用,该类又包含对用于加载应用程序的java.lang.Classloader实例的引用。这反过来意味着垃圾收集算法无法回收空间。
并且java.lang.ClassLoader 的那个实例仍然引用应用程序的所有类,通常在 PermGen 中占用数十兆字节。这意味着只需重新部署几次即可填充通常大小的 PermGen 并在日志中获取java.lang.OutOfMemoryError: PermGen space错误消息。
3,解决办法是什么?
1.解决初始化时OutOfMemoryError
当应用程序启动时触发由于 PermGen 耗尽导致的 OutOfMemoryError 时,解决方案很简单。应用程序只需要更多空间将所有类加载到 PermGen 区域,所以我们只需要增加它的大小。为此,请更改您的应用程序启动配置并添加(或增加(如果存在))类似于以下示例的-XX:MaxPermSize参数:
java -XX:MaxPermSize=512m com.yourcompany.YourClass
上述配置将告诉 JVM,允许 PermGen 增长到 512MB,然后才能开始以 OutOfMemoryError 的形式抱怨。
2.解决重新部署时OutOfMemoryError
当您重新部署应用程序后立即发生 OutOfMemoryError 时,您的应用程序会遭受类加载器泄漏。在这种情况下,解决问题的最简单,最直接的方式就是用工具排查,找到有问题的代码,并解决它以分钟为单位。
对于那些不能使用 Plumbr 或决定不使用的人,也可以使用替代方法。为此,您应该继续进行堆转储分析 - 在重新部署后使用类似于以下命令的命令进行堆转储:
jmap -dump:format=b,file=dump.hprof
然后使用您最喜欢的堆转储分析器打开转储(Eclipse MAT 是一个很好的工具)。在分析器中,您可以查找重复的类,尤其是那些加载应用程序类的类。从那里,您需要进入所有类加载器以找到当前活动的类加载器。
对于不活动的类加载器,您需要通过从不活动的类加载器获取到GC 根的最短路径来确定阻止它们被垃圾收集的引用。有了这些信息,您就会找到根本原因。如果根本原因在 3rd 方库中,您可以继续访问 Google/StackOverflow 以查看这是否是获取补丁/解决方法的已知问题。如果这是您自己的代码,则需要删除违规引用。
3.解决运行时OutOfMemoryError
对于那些再次无法使用 Plumbr 的人,也可以使用另一种方法。在这种情况下,第一步是检查是否允许 GC 从 PermGen 卸载类。标准的 JVM 在这方面相当保守——类天生就是为了永生。所以一旦加载,即使没有代码再使用它们,类也会留在内存中。当应用程序动态创建大量类并且长时间不需要生成的类时,这可能会成为一个问题。在这种情况下,允许 JVM 卸载类定义会很有帮助。这可以通过向启动脚本添加一个配置参数来实现:
-XX:+CMSClassUnloadingEnabled
默认情况下,它设置为 false,因此要启用它,您需要在 Java 选项中显式设置以下选项。如果您启用CMSClassUnloadingEnabled,GC 也会清除PermGen 并删除不再使用的类。请记住,此选项仅在使用以下选项启用UseConcMarkSweepGC时才有效。因此,当运行ParallelGC或,上帝保佑,Serial GC 时,请确保您已通过指定将 GC 设置为CMS:
-XX:+UseConcMarkSweepGC
在确保可以卸载类并且问题仍然存在后,您应该继续进行堆转储分析 - 使用类似于以下的命令进行堆转储:
jmap -dump:file=dump.hprof,format=b
然后使用您最喜欢的堆转储分析器(例如 Eclipse MAT)打开转储,并根据加载的类数量继续查找最昂贵的类加载器。从这样的类加载器中,您可以继续提取加载的类并按实例对此类类进行排序,以获得可疑的顶部列表。
对于每个嫌疑人,您需要手动将根本原因追溯到生成此类类的应用程序代码。
Java OOM系列专题:
第一篇:Java OOM 原理篇 : 什么是 Java OOM
第二篇:Java OOM 基础篇:常见的OutOfMemoryError 场景一:Java heap space 堆溢出问题详解
第三篇:Java OOM 基础篇:常见的OutOfMemoryError 场景二 : GC overhead limit exceeded 问题详解
第四篇:Java OOM 基础篇:常见的OutOfMemoryError 场景三: PermGen space 永久空间问题详解
第五篇:Java OOM 基础篇:常见的OutOfMemoryError 场景四: Permgen size 元空间问题详解
第六篇:Java OOM 实战篇:应用故障之Java heap space 堆溢出实战
第七篇:Java OOM 高级篇:体验了一把线上CPU100%及应用OOM的排查和解决过程
第八篇:Java OOM 高级篇:线上Docker 上Springboot程序OOM问题的排查分享