关于类加载器内存泄露的分析

标签: 加载器 内存泄露 分析 | 发表时间:2014-04-26 12:55 | 作者:
出处:http://www.iteye.com

从上个世纪90年代Java诞生之日起,Java的类和资源的加载就一直是个问题。由于它增加了启动和初始化时间,因此这个问题在Java应用服务器上则尤为明显。为了缓解这个问题,大家试过了不同的访问,比如说以exploaded方式部署,但这只对简单的应用有效;还有2001年发明的Java热插拔的机制。启用热插拔的话,你在一个现有的方法内的改动马上就会生效。由于方法的边界限制,这个方法并不是特别有用,通常它只是在调试的阶段使用。对于现在的应用来说,编译,部署以及重启,等待个5到15分钟已经不是什么稀奇事儿了。越大型的应用服务器,这种情况可能就越明显。

存在的问题

一旦某个Java类被类加载器加载了,它就是不可变的,只要类加载器还存在,它也会一直存在下去。类的唯一标识是它的类名以及类加载器的标识,要重启一个应用的话,你需要创建一个新的类加载器,并加载最新版本的类。你不能把一个已经存在的对象映射到一个新类上面,因此重新加载时的状态迁移非常重要。这意味着你得初始化应用和配置的状态,拷贝用户的会话信息,以便重新生成整个应用的对象图。通常来说这非常耗时并很容易产生内存泄露。

说到类加载器的内存泄露,由于Java使用的内存模型的原因,哪怕是一小行代码的泄露都会产生很大的影响。比如说,一个类加载器的实例,它拥有自己加载的所有类的引用,以及这些类生成的所有对象的引用。因此在应用重启过程的状态迁移中,哪怕一个很小的泄露,都可能会产生极大的影响。

那这些对开发人员来说意味着什么?它意味即使是普通的编译,构建,打包,部署,应用重启,这些琐事都会极大的分散你的注意力,影响你的开发效率。

本文试图揭秘对开发人员而言JRebel所带来的威力,看一下这个产品背后究竟有什么奥妙,以及深入了解下JVM的那些你可能会忽略的地方 。本文主要关注JRebel所试图要解决的那些问题。


认识类加载器

类加载器只是一个普通的Java对象

是的,它并不是什么了不起的东西,除了JVM的系统类加载器,剩下的全都是一个普通的Java对象而已!ClassLoader是一个抽象类,你可以自己创建一个类来实现它。下面是它的API:

public abstract class ClassLoader {  public Class loadClass(String name);
  protected Class defineClass(byte[] b);
  public URL getResource(String name);
  public Enumeration getResources(String name);
  public ClassLoader getParent();
} 



看起来相当简单,对吧?我们来逐个看下这些方法。最核心的方法是loadClass,它接受一个String类型的类名,并且返回实际的Class对象。如果你之前用过类加载器的话,这可能是你最熟悉的一个方法了,因为你可能每天都会用到它。defineClass是一个final类型的方法,它接受一个来自文件或者网络的byte数组,返回的也是一个Class对象。

类加载器还会从类路径中加载资源。它的工作方式和loadClass方法差不多。类似的方法有好几个,比如getResource和getResources,它返回的是一个URL对象,或者是一个URL的Enumeration。这些URL指向的是方法参数name中对应的资源。

每个类加载器都会有一个父类加载器,getParent方法返回的就是这个父加载器,它和Java的继承没有什么关系,只是用一个链表将它们串联起来而已。后面我们会稍微深入的了解下它。

类加载器是懒加载模式的,因此类只有在运行时被请求加载的话才会被加载进来。类是由调用到它的对象加载的,因此在运行时一个类可能会被多个类加载器加载,这取决于具体是哪个类引用到了它们以及哪个类加载器加载了引用了它们的类。。。好吧,我自己都有点绕晕了。我们来看段代码吧。

public class A {
  public void doSmth() {
    B b = new B();
    b.doSmthElse();
  }
}


这里有一个A类,它在doSmth()方法里调用了B类的构造方法。实际上底层会触发这样的调用:

A.class.getClassLoader().loadClass(“B”);


加载了A类的类加载器会去加载B类。

类加载器是分层的,不过跟孩子们不一样,它们不会总听父母的话

每个类加载器都会有一个父加载器。当请求一个类加载器加载类时,它通常会先调父类加载器的loadClass方法,而它的父类加载器也会再去找自己的父加载器,这么一直下去。如果同一个父加载器下面有两个类加载器,它们又同时被请求加载同一个类,类加载器只会加载一次。如果两个类加载器分别加载了同一个类,事情就会变得非常麻烦,下面我们会看到这种情况。

Java应用服务器在实现Java EE规范的时候,有的实现是先委托给父加载器进行加载,有的实现则会先看下本地的Web应用类加载器底下有没有。我们来深入分析下这种情况,下面用图1作为例子。



在这个例子中,模块WAR1有自己的类加载器,它会优先用它来加载类,而不是委托给自己的双亲,也就是App1.ear的类加载器。这意味着不同的WAR模块,比如WAR1和WAR2,它们互相看不到对方的类。App1.ear模块有自己的类加载器,并且它是WAR1和WAR2类加载器的父加载器。当WAR1和WAR2的类加载器需要向上委派加载请求时,它会去请求App1.ear的类加载器,这意味着要加载的类在WAR类加载器的作用域外。如果某个类在WAR和app1中同时在在的话,WAR中的会覆盖掉APP的。最后EAR的类加载器的双亲就是容器的类加载器。EAR类加载器会把请求委派给容器的类加载器,不过它和WAR的做法并不一样,它会优先委派给父加载器。正如你所看到的,现在情况变得有点复杂了,这和普通的Java SE中的类加载行为并不一致。

那么在应用中如何重新加载类呢?

从前面的ClassLoader的API那可以知道,它只能用来加载类。也就是说,它没法用来卸载,或者重新加载类,因此如果要在运行时重新加载一个类的话,你得把现有的整个类结构体系全部扔掉,然后再重新加载使用,就像图2中那样。



如果你已经用过一段时间的Java了,你肯定会知道这要发生内存泄露了。一般的内存泄露是因为集合里面引用了许多需要要被清除的对象,但最终却没有被清理掉。类加载器也是这种情况,不过它更特殊一点。不幸的是,从Java平台的当前情况来看,这种情况不可避免并且开销极大。在经过几次重新部署后最终会抛出OutOfMemoryErrors异常。

每一个对象都会有一个指向自己对应类的引用,而这个类又会引用它的类加载器。关键在于类加载器又有它加载过的所有类的引用,每个类里面又会有一些静态的字段,像图3中那样。




这意味着:

1. 如果类加载器泄露了,它所持有的所有类对象以及它们的静态字段也都会泄露。静态字段一般来说是些缓存,单例对象,以及不同的配置及应用状态信息。就算你的程序本身并没有任何大的静态缓存,这并不意味着你的框架不会替你缓存些什么东西(比如说log4j,它一般都在容器的类路径底下)。这同时也说明了为什么类加载器一旦泄露就会非常严重。

2. 只要有一个对象泄露了,那么它对应的类的类加载器就会跟着一起泄露。尽管这个对象可能看起来占不了什么地方(它可能连一个字段都 没有),但它仍会引用到它自己的类加载器,最终引用到所有相关的应用状态信息。在应用重新部署的过程中,只要有一个地方发生了泄露,没有正确的清理掉,就会导致严重的泄露问题。通常一个应用中会有好几处类似会泄露的地方,由于一些第三方库本身构建的问题,有一些泄露的问题几乎无法解决。因此,类加载器的泄露十分常见。

这就是类加载器背后的技术难点,也就是说为了能在运行时刷新我们的代码,通常都得重新编译打包,部署甚至重启服务才能看到更新的代码。下篇文章中我们将会讲到Java中的这个难题的一些解决方案,包括使用Java 1.4中引入的一个类热插拔的框架,以及JRebel。

原创文章转载请注明出处:
http://it.deepinmind.com

英文原文链接


已有 0 人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



相关 [加载器 内存泄露 分析] 推荐:

关于类加载器内存泄露的分析

- - ITeye博客
从上个世纪90年代Java诞生之日起,Java的类和资源的加载就一直是个问题. 由于它增加了启动和初始化时间,因此这个问题在Java应用服务器上则尤为明显. 为了缓解这个问题,大家试过了不同的访问,比如说以exploaded方式部署,但这只对简单的应用有效;还有2001年发明的Java热插拔的机制.

使用MAT分析内存泄露

- - Taobao QA Team
对于大型服务端应用程序来说,有些内存泄露问题很难在测试阶段发现,此时就需要分析JVM Heap Dump文件来找出问题. 随着单机内存越来越大,应用heap也开得越来越大,动辄十几G的Dump也不足为奇了. 要快速分析,快速定位问题就必须有给力的工具帮忙,下面我来介绍下常用内存分析工具. JDK自带的一个工具,是JVM Heap导出的必备工具.

比较隐蔽的内存泄露案例分析

- - 百度质量部 | 软件测试 | 测试技术 | 百度测试|QA
和大家分享一个笔者在真实项目中遇到的一个内存泄露真实案例.. 真实项目中的一个待测模块,这里简化一下,整体可以看做是:输入—中间处理–输出模式,如下图1-1. 图1-1 待测模块整体框图. 其中中间处理与模块业务相关,这里我们可以暂时看做黑盒,不必关心. A. 用valgrind跑程序报警图如下1-2,但是报警所指的地方,研发者坚持认为已经释放,是valgrind误报.

Android 性能优化之使用MAT分析内存泄露问题

- - CSDN博客推荐文章
转载请注明本文出自xiaanming的博客( http://blog.csdn.net/xiaanming/article/details/42396507),请尊重他人的辛勤劳动成果,谢谢. 内存泄露就像一个定时炸弹,随时都有可能使我们的应用程序崩溃掉,所以作为一名Android开发人员,还是需要有分析内存泄露的能力,说道这里我们还是要说下什么是内存泄露,内存泄露是指有个引用指向一个不再被使用的对象,导致该对象不会被垃圾回收器回收.

Java常见问题分析(内存溢出、内存泄露、线程阻塞等)

- - Java - 编程语言 - ITeye博客
Java垃圾回收机制(GC) . 堆内存3代分布(年轻代、老年代、持久代) . ML(内存泄露) OOM(内存溢出)问题现象及分析 . IBM DUMP分析工具使用介绍. Java应用CPU、线程问题分析. Java垃圾回收机制(GC). 1.GC机制作用 . 1.1 JVM自动检测和释放不再使用的对象内存 .

一次大量 JVM Native 内存泄露的排查分析(64M 问题)

- - 掘金后端本月最热
我们有一个线上的项目,刚启动完就占用了使用 top 命令查看 RES 占用了超过 1.5G,这明显不合理,于是进行了一些分析找到了根本的原因,下面是完整的分析过程,希望对你有所帮助. Linux 经典的 64M 内存问题. 堆内存分析、Native 内存分析的基本套路. tcmalloc、jemalloc 在 native 内存分析中的使用.

关于内存泄露

- - 银河里的星星
valgrind 详细说明  http://www.cnblogs.com/wangkangluo1/archive/2011/07/20/2111273.html. 近期Imgsrc一处内存泄露问题的查找和解决  http://rdc.taobao.com/blog/cs/?p=1651.

ios Instruments 内存泄露

- - ITeye博客
虽然iOS 5.0版本之后加入了ARC机制,由于相互引用关系比较复杂时,内存泄露还是可能存在. 这里讲述在没有ARC的情况下,如何使用Instruments来查找程序中的内存泄露,以及NSZombieEnabled设置的使用. 本文假设你已经比较熟悉Obj-C的内存管理机制. 实验的开发环境:XCode 4.5.2.

C++检查内存泄露

- - CSDN博客推荐文章
说明,我使用的ide是vs2008. 内存泄露的检测一般在debug模式下进行. 2.在需要检查内存泄露的cpp头部加上. 4.然后就可以在输出中看泄露情况了. 举个例子,例子中我用newEx表示的上述宏定义中的new. 输出中显示的内容(debug下运行程序,然后点叉叉关闭程序).   Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD .

Java的内存泄露

- - Java译站
Java有垃圾回收,因此不会出现内存泄露. 尽管Java的确有垃圾回收器来回收那些不用的内存块,但你不要指望它能够点铁成金. GC减轻了开发人员肩上的负担,而原本的那些工作非常容易出错,不过并不是所有内存分配的问题它都能够解决. 更糟糕的是,Java的设计允许它可以欺骗GC,使得它能够保留一些程序已经不再使用的内存.