内存溢出之PermGen OOM深刻分析

如今,网上关于讨论PermGen OOM的资料不少,可是深刻分析PermGen区域内存溢出缘由的资料不多。本篇文章尝试全面分析一下PermGen OOM的缘由,其中涉及到了Java虚拟机运行时数据区、类型装载、类型卸载等,测试代码涉及到了JMX协议。相关前提知识以下:
       一、Java类加载的基本原理
       二、Java类型卸载相关的知识,http://www.blogjava.net/zhuxing/archive/2008/07/24/217285.html
       三、简要了解JMX协议,有关JMX协议能够参加sun公司发布的技术规范,对JMX协议作必定的了解对理解Java性能监控和调优功能的实现原理有很大帮助。

        
       【虚拟机运行时数据区介绍】
    
本部分将对Java虚拟机运行时数据区作一个简单的介绍,着重说明PermGen区域(永久存储区)存放的内容,并对运行时数据区的访问方式作一个概括说明,为后面深刻分析类型卸载和PermGen OOM作铺垫。为了更具备通用性,本部分将更多关注虚拟机协议自己,可能和具体的虚拟机实现有少量的出入。

        【运行时数据区分类】

        Java虚拟机的运行时数据区通常分类以下(不必定是物理划分):   html

  •     堆:主要存放对象实例,线程共享
  •     栈:主要存储特定线程的方法调用状态,线程独占
  •     本地方法栈:存储本地方法的调用状态,线程独占
  •     PC寄存器:学过操做系统课程的都知道,线程独占
  •     方法区:主要存储了类型信息,线程共享 

            方法区能够简单的等价为所谓的PermGen区域(永久存储区),在不少虚拟机相关的文档中,也将其称之为"永久堆"(permanent heap),做为堆空间的一部分存在。介于此,咱们能够简单说明一下咱们经常使用的几个堆内存配置的参数关系:
        *-XX: PermSize:*永久堆(Pergen区域)大小默认值
        *-XX:MaxPermSize:*永久堆(Pergen区域)最大值
        *-Xms:*堆内存大小默认值
        *-Xmx:*堆内存最大值

            【运行时数据区访问方式总结】
    java

        从开发者角度,虚拟机运行时数据区的访问方式简要概括以下:bootstrap

  •     活动的线程能够经过对应的栈来访问运行时数据区信息
  •     栈是堆访问的入口
  •     堆上Java.lang.Class实例是访问PermGen区域中类型信息的入口 
             
    1. 一个类型装载以后会建立一个对应的java.lang.Class实例,这个实例自己和普通对象实例同样存储于堆中,我以为之因此说是这是一种特殊的实例,某种程度上是由于其充当了访问PermGen区域中类型信息的代理者。
    2. 图中"Class类型实例"和"类加载器实例"分别是A类型对应的java.lang.Class实例和加载A类型的类加载器实例。
    3. 只要是有active的对象实例句柄,就可以访问到对应的Class类型实例和类加载器实例,分别经过Object.getClass()方法和Class.getClassLoader()方法。
    4. 只要是有active的Class类型实例句柄,就可以访问到对应的类加载器实例。

            【PermGen内存溢出深刻分析】

            【前提知识】    缓存

  •     由不一样的类加载器实例加载的类型能够等价为彻底不一样的类型,哪怕时同一类型类加载器的不一样实例加载的,都会在PermGen区域分配相应的空间来存储类型信息
  •     新类型加载时,会在PermGen区域申请相应的空间来存储类型信息,类型被卸载后,PermGen区域上的垃圾收集会释放对应的内存空间。PermGen区域和普通的堆空间同样,也遵循垃圾收集的规律,因此,网上不少资料种关于PermGen区域空间的大小是只增不减的说法是不正确的,后面会用相应的测试代码来验证和分析。
  •     一种类型被卸载的前提条件是:加载此类型的类加载器实例变为不可达(unreachable)状态,虚拟机协议中对应描述以下:
        A class or interface may be unloaded if and only if its class loader is unreachable. The bootstrap class loader is always reachable; as a result, system classes may never be unloaded.
    关于实例的*unreachable*状态,大体能够理解为不能经过特定活动线程对应的栈出发经过引用计算来到达对应的实例,虚拟机协议中对应描述以下:
        _A reachable object is any object that can be accessed in any potential continuing
    computation from any live thread._
        结合上面的[虚拟机运行时数据区的介绍|],能够得出结论:类型对应的普通实例、类型对应的java.lang.Class实例、加载此类型的ClassLoader实例,三者中有任何一种或者多种是reachable状态的,那么此类型就不可能被卸载。
  •     JMX协议提供了相应的API接口,用来在运行时查询当前虚拟机实例的内存使用和类型加载等信息。这也是不少Java性能监控和分析工具的基础,后面的测试程序中也有相应的代码使用了JMX协议。

            
                【测试程序分析】       工具

  • 虚拟机器参数设置以下:
        -XX: PermSize=4M -XX:MaxPermSize=4M -verbose -verbose:gc
        设置-verbose参数是为了获取类型加载和卸载的信息
        设置-verbose:gc是为了获取垃圾收集的相关信息
  • 在D:/classes目录下有一个简单的类型ZhuXing对应的class字节码,测试代码中用URLClassLoader来加载此类型

     

            【测试程序一:模拟PermGen OOM】性能

     1  try  {
     2      // 准备url
     3     URL url  =   new  File( " D:/classes " ).toURL();
     4     URL[] urls  =  {url};
     5 
     6      // 获取有关类型加载的JMX接口
     7      ClassLoadingMXBean loadingBean  =  ManagementFactory.getClassLoadingMXBean();
     8 
     9      // 用于缓存类加载器
    10      List < ClassLoader >  classLoaders  =   new  ArrayList < ClassLoader > ();
    11 
    12      while  ( true ) {
    13        // 加载类型并缓存类加载器实例
    14         ClassLoader classLoader  =   new  URLClassLoader(urls);
    15        classLoaders.add(classLoader);
    16        classLoader.loadClass( " ZhuXing " );
    17 
    18        // 显示数量信息(共加载过的类型数目,当前还有效的类型数目,已经被卸载的类型数目)
    19        System.out.println( " total:  "   +  loadingBean.getTotalLoadedClassCount());
    20  System.out.println( " active:  "   +  loadingBean.getLoadedClassCount());
    21       System.out.println( " unloaded:  "   +  loadingBean.getUnloadedClassCount());
    22     }
    23  catch  (Exception e) {
    24     e.printStackTrace();
    25  }
    26 
    27 

         【测试程序一分析
    运行测试程序一,输出信息以下(摘取了部分):
    ......
    [Loaded ZhuXing from [file:/D:/classes/]]
    total: 2914
    active: 2914
    unloaded: 0
    [Loaded ZhuXing from [file:/D:/classes/]]
    total: 2915
    active: 2915
    unloaded: 0
    [Full GC 4852K->4852K(8720K), 0.0993780 secs]
    [Full GC 4852K->4829K(8720K), 0.0999775 secs]
    [Full GC 4829K->4829K(8720K), 0.0989805 secs]
    [Full GC 4829K->4829K(8720K), 0.0997261 secs]
    ......
    Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
    ......
    [Unloading class ZhuXing]
    ......
    [Loaded java.lang.Shutdown from D:\eos6\jdk1.5.0_09\jre\lib\rt.jar]
    [Loadedjava.lang.Shutdown$Lockfrom D:\eos6\jdk1.5.0_09\jre\lib\rt.jar

        
            针对以上摘录的虚拟机器运行时信息,分析结论以下:测试

  •     一直在持续的加载类型ZhuXing,并且一直没有卸载,直到PermGen OOM发生。类型ZhuXing没法卸载的缘由,前面说明过,是因为对应的类加载器实例一直是reachaable状态,缓存对象实例或者java.lang.Class实例一样能够达到没法卸载类型的效果。
  •     在PermGen OOM发生前,虚拟机进行了很是频繁的垃圾收集,效果甚微
  •     在PermGen OOM发生后,卸载了类型ZhuXing,当前虚拟机实例退出


            【测试程序二:PermGen区域垃圾收集】ui

                和测试程序一相比,删除了类加载器实例缓存的代码
     1  try  {
     2        // 准备url
     3       URL url  =   new  File( " D:/classes " ).toURL();
     4       URL[] urls  =  {url};
     5 
     6        // 获取有关类型加载的JMX接口
     7        ClassLoadingMXBean loadingBean  =  ManagementFactory.getClassLoadingMXBean();
     8 
     9        while  ( true ) {
    10        // 加载类型,不缓存类加载器实例
    11         new  URLClassLoader(urls).loadClass( " ZhuXing " );
    12        // 显示数量信息(共加载过的类型数目,当前还有效的类型数目,已经被卸载的类型数目)
    13        System.out.println( " total:  "   +  loadingBean.getTotalLoadedClassCount());
    14       System.out.println( " active:  "   +  loadingBean.getLoadedClassCount());
    15       System.out.println( " unloaded:  "   +  loadingBean.getUnloadedClassCount());
    16      }
    17  catch  (Exception e) {
    18      e.printStackTrace();
    19  }
    20 
    21 

    测试程序二分析
    运行测试程序二很长时间,一直没有发生PermGen OOM异常,输出信息以下(摘取了部分):
    ...
    [Loaded ZhuXing from [file:/D:/classes/]]
    total: 19540
    active: 1052
    unloaded: 18488
    [Full GC 1563K->259K(2112K), 0.1758958 secs]
    ......
    [Unloading class ZhuXing]
    [Unloading class ZhuXing]
    [Unloading class ZhuXing]
    ......
    [GC 1968K->1563K(2112K), 0.0025266 secs]
    ......
    [Loaded ZhuXing from [file:/D:/classes/]]
    total: 21098
    active: 440
    unloaded: 20658
    ...
    针对以上摘录的虚拟机器运行时信息,分析结论以下:url

    1. 类型ZhuXing在频繁被加载的同时,也在频繁被卸载,当被加载的类型达到了21098时,并无发生PermGen OOM,20658已经被卸载,堆内存的占用比测试代码一中小的多
    2. 中间进行的垃圾并非特别频繁,可是垃圾收集的效果较为明显
    3. 类型被卸载以后,伴随着PermGen区域上的垃圾收集和新类型的不断被加载,PermGen区域中类型信息占有的堆内存大小在有序的增大减少

            【PermGen OOM缘由总结】
                经过上面的[测试程序分析|],咱们发现PermGen OOM发生的缘由和类型装载、类型卸载有直接的关系,能够对PermGen OOM发生的缘由作以下大体的总结:
            一、PermGen区域分配的堆空间太小,能够经过设置-XX: PermSize参数和-XX:MaxPermSize参数来解决。
  •         二、类型卸载不及时,过期无效的类型信息占用了空间,咱们不妨称其为"永久堆"的内存泄漏,须要经过深刻分析类型卸载的原理来寻找对应的防范措施spa


            【常见的类加载器和类型卸载的可能性总结】
            经过前面的讨论,咱们知道若是加载某种类型的类加载器实例没有处于unreachable状态,则该类型就不会被卸载,该类型不被卸载,则对应的类型信息在PermGen区域中占有的堆内存就不会被释放。下面,针对典型的Java应用分类,分析一下经常使用类加载器加载的类型被下载的可能性。

            【普通Java应用】
            启动类加载器:因为其负责加载虚拟机的核心类型,因此由其加载的类型在整个程序运行期间不可能被卸载,对应类型信息占用的PermGen区域堆空间不可能获得释放。
            扩展类加载器:负责加载JDK扩展路径下的类型,扩展类加载器同时又做为系统类加载器的父类加载器,因此,由其加载的类型在整个程序运行期间基本上不可能被卸载,对应类型信息占用的PermGen区域堆空间基本不可能获得释放。
            系统类加载器:负责加载程序类路径上面的类型,由其加载的类型在整个程序运行期间基本上不可能被卸载,对应类型信息占用的PermGen区域堆空间基本不可能获得释放。
            用户自定义类加载器:对于其加载的类型,知足类型卸载要求的可能性比较容易控制,只要是其实例自己处于unreachable状态,其加载的类型会被卸载,PermGen区域中对应的空间占有也会被释放。


            【插件开发】
            系统类加载器:因为其负责加载虚拟机的核心类型,因此由其加载的类型在插件应用运行期间不可能被卸载,对应类型信息占用的PermGen区域堆空间不可能获得释放。
            插件类加载器:系统插件类加载器负责加载OSGI实现的相关类型,因此由其加载的类型在插件应用运行期间不可能被卸载;用户开发的插件所使用的默认插件类加载器,和特定的插件自己进行域绑定,插件之间存在必定的类型引用关系,而且特定插件在整个插件应用的运行时被中止的可能性也很小,因此类型卸载发生概率极小。
            用户自定义类加载器:对于其加载的类型,知足类型卸载要求的可能性比较容易控制,只要是其实例自己处于unreachable状态,其加载的类型会被卸载,PermGen区域中对应的空间占有也会被释放。

            【PermGen内存溢出的应对措施】
         
    经过上面的PermGen OOM的缘由的分析,不难看出对应的应对措施:

  •     合理的设置-XX: PermSize和-XX:MaxPermSize参数(主要的有效措施)
  •     有效的利用的虚拟机类型卸载的机制(针对程序进行调优)


            【合理设置参数(针对普通用户和开发者)】
            经过设置合理的XX: PermSize和-XX:MaxPermSize参数值是减小和有效避免PermGen OOM发生的最有效最主要的措施,尤为是针对普通用户而言,这基本上是惟一的办法。关于合理设置这两个参数,建议以下:

  •      XX: PermSize参数的设置尽可能创建在基准测试的基础之上,能够利用监控工具对稳定运行期间PermGen区域的大小进行统计,取合理的平均值。网上的不少资料中,建议XX: PermSize和XX:MaxPermSize设置为相同的数值,我的以为这是不正确的,由于两个参数的出发点是不同的。XX: PermSize设置的过大确定会在应用运行的大部分时间中浪费堆内存,有可能会明显增长存放普通对象实例的堆空间的垃圾收集的次数。

  •     XX:MaxPermSize参数的设置应该着眼于PermGen区域使用的峰值,由于这是避免PermGen OOM的最后一道屏障,其设置最好也是创建在性能监控工具的统计结果之上。
  •     和虚拟机有关的性能参数较多的分为两类,一类是初始值或默认值,一类是峰值。若是该性能参数是会涉及到的虚拟机垃圾收集机制的,关于初始值或者默认值的设置尽可能要创建在测试基础之上,尽可能作到在单次垃圾收集时间和垃圾收集频率之间保持一个平衡,不然颇有可能拔苗助长。


             【有效利用虚拟机类型卸载机制(针对开发者)】
            此部分的建议能够做为开发者进行性能调优或者平常开发时候的参考,尽可能可以配合相应的性能监控工具进行:   

  •     检查是否因为程序设计自己上的缺陷,致使加载了大量实际上并不须要的类型。较新版本的Java虚拟机实现,通常都遵循动态解析的建议,因此不是人为设计的缺陷,通常不会诱发加载了大量实际上并不须要的类型。结合插件开发的应用场景,我的以为插件功能模块的划分(其中包括了插件依赖关系的设计和有关扩展点的扩展收集等)和第三方jar的使用多是诱发此问题的两个重要根源。
  •     对象缓存的使用是否得当,经过前面的分析,咱们知道这多是致使类型不能被卸载的重要缘由。缓存的使用,既要认识到其能够提升时间性能的有点,也要分析其可能会给普通对象堆空间和PermGen区域形成的负担。
  •     自定义类加载器的合理使用,相关的几个注意要点包括:
    1. 是否不恰当的利用的类型更新的特性,也就是说是否对类加载器实例的unreachable状态作了有效的判断。考虑以下场景,假设用户开发了一个自定义类加载器来加载工程输出目录下的临时类型,对临时类型作了没必要要的缓存,这确定会致使全部被加载过的临时类型都不会获得卸载,会直接加剧PermGen区域的负担。
    2. 自定义类加载器和其余已有类加载器的协做关系是否合理,是否合理的利用了Java类加载的双亲委派机制。咱们知道,不一样的类加载器实例(哪怕是同一种类加载器类型的不一样实例)加载的同一种自定义类型在虚拟机内部都会被放置到不一样的命名空间中做为不一样类型来处理,因此合理的设置父类加载器变得很重要,不合理的设置会致使大量没必要要的"新"类型被创造出来,何况这些没必要要的"新"类型是否可以被及时卸载仍是个未知数。
  •     慎重检查自定义类加载器实例是否被不恰当的缓存了,缘由不言而喻。  【后记】 写这篇文章的初衷是为了深刻的分析PermGen OOM发生的缘由,在深刻分析的基础之上理解PermGen OOM的应对措施,从"为何会发生PermGen OOM"到"到底为何会发生PermGen OOM"。但愿对你们更深刻的认识PermGen OOM和PermGen OOM的应对措施起到做用,谢谢!
  • 相关文章
    相关标签/搜索