`
deepinmind
  • 浏览: 451567 次
  • 性别: Icon_minigender_1
  • 来自: 北京
博客专栏
1dc14e59-7bdf-33ab-841a-02d087aed982
Java函数式编程
浏览量:41631
社区版块
存档分类
最新评论

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

阅读更多

从上个世纪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

英文原文链接
3
0
分享到:
评论

相关推荐

    lassLoader的关系以及如何防止ClassLoader内存泄漏

    内存泄漏通常发生在类加载器生命周期结束后,但其所加载的类和资源仍被引用,无法被垃圾收集器回收。以下是一些可能导致ClassLoader内存泄漏的场景和预防措施: 1. 静态变量引用:静态变量会随着类的加载而存在,...

    Java的类加载器

    这导致了类加载器的泄漏问题,需要注意对类加载器的生命周期管理。 7. **类加载器的源码分析** 对于深入理解类加载器的工作原理,阅读和分析JDK源码是非常有帮助的。例如,`java.lang.ClassLoader`的`loadClass()`...

    黑马程序员------类加载器学习注意点

    不正确的类加载器管理可能导致内存泄漏。当一个类加载器不再使用时,如果没有其他引用指向它,理论上应该被垃圾回收。但是,如果这个类加载器加载的类还在使用,那么类加载器就无法被回收,这可能会导致资源浪费。...

    ThreadLocal 内存泄露的实例分析1

    然而,如果存在对 WebappClassLoader 的强引用,那么这个类加载器就无法被回收,进而导致了内存泄漏。 在描述的案例中,`LeakingServlet` 是一个使用了 `ThreadLocal` 的 Servlet。`ThreadLocal` 是 Java 中用于在...

    Android 内存泄露 Mat工具分析

    本文将详细介绍如何使用 MAT 分析 Android 应用程序中的内存泄露,特别是如何根据 heap dump 分析泄漏根源。 ClassLoader 和 Java 类加载机制 在介绍 MAT 之前,需要了解 Java 类加载机制。ClassLoader 是 Java 中...

    Java面试题-内存+GC+类加载器+JVM调优.pdf

    在 Java 面试中,内存、GC、类加载器和 JVM 调优是非常重要的知识点,本文将对这些知识点进行详细的解释和分析。 一、Java 内存模型 在 Java 中,内存主要分为两部分:堆(Heap)和栈(Stack)。堆是存储对象的...

    Java类重新加载101对象类和类加载器Java开发Jav

    因此,合理地管理和设计自定义类加载器是防止内存泄漏的关键。 在“Java类重新加载101对象类和类加载器”的主题中,开发者还需要理解类加载的生命周期、类的可见性以及类加载器的层次结构。这些都是深入学习Java...

    类加载机制与JDK调优命令.pdf

    首先,类加载器将.class文件加载到内存中;接着,进行连接操作,包括验证字节码文件的正确性、为类的静态变量分配内存并赋予默认值;然后,加载类所引用的其它所有类,并为类的静态变量赋予真正的初始值;之后,执行...

    java内存泄露深度分析及解决

    类加载器是另一个复杂且可能导致内存泄漏的领域。每个类加载器都会加载特定的类,并保持对这些类的引用,直到类加载器本身被卸载。如果类加载器生命周期过长,或者与应用的其他部分有强引用,会导致类加载器无法被...

    java中关于类加载的资料

    - 适当使用类加载器,避免类加载器泄露导致内存泄漏。 - 理解并利用双亲委派模型,确保类的唯一性。 - 在设计复杂的系统时,考虑使用自定义类加载器以实现特定的加载逻辑。 总之,理解Java中的类加载机制有助于...

    类加载机制PPT+代码

    类加载机制是Java虚拟机(JVM)运行时的核心机制之一,它负责将类的.class文件从磁盘或网络中加载到内存,...通过深入学习和实践,我们可以更好地优化程序性能,避免内存泄漏,甚至实现自定义类加载器以满足特定需求。

    SWT内存泄露解决办法

    在提供的"内存泄露.txt"文件中,可能包含关于SWT内存泄露更具体的案例和解决方案。而"org.eclipse.swt.sleak_1.0.2.zip"则可能是包含SWT内存泄露检测工具或示例代码的资源包,解压后可以进一步研究和实践。 总结来...

    java内存泄漏问题追踪

    - 类加载器泄漏:类加载器未正确卸载,其所加载的类无法被卸载,进而导致内存泄漏。 - 本地方法栈内存泄漏:Java Native Interface (JNI) 使用的本地方法可能会占用C/C++堆内存,如未释放,可能导致内存泄漏。 2....

    JProfiler对应用服务器内存泄漏问题诊断

    还有类加载器视图,可用于检查类加载的异常情况。 五、注意事项 - 在生产环境中使用JProfiler时,要注意对应用服务器的影响,最好在非高峰期进行诊断。 - 内存泄漏的检测并非一次性任务,应定期进行,尤其是当性能...

    jvm知识点总览(类的加载机制+内存结构+GC算法 垃圾回收+GC分析 命令调优)

    类加载器是这一过程的关键参与者,主要有三种:启动类加载器加载JDK核心库,扩展类加载器加载扩展库,应用类加载器则加载用户类路径上的类。类加载机制遵循全盘负责、父类委托和缓存机制的原则。 JVM内存结构分为堆...

    内存泄露方面分析文档

    在Java 5 SR2及更早版本中,也可能遇到类加载器耗尽的情况。如果问题出在原生堆上,将深入到流程图底部的蓝色框中。如果缺少Java核心转储,可以使用最后一次详细的垃圾回收(Verbose GC)周期或进程地址空间监控。 ...

    mac_mat android内存泄漏分析工具

    "mac_mat android内存泄漏分析工具"就是这样一个针对Android平台的工具,特别适合在Mac环境下对应用进行内存泄漏检测。 MAT(Memory Analyzer Tool)是由Eclipse基金会开发的一个强大的Java内存分析工具。尽管主要...

    内存泄露的例子

    3. 类加载器:如果类加载器没有被卸载,其所加载的类和相关资源也会持续占用内存。 4. 缓存:过度使用缓存,尤其是无限增长的缓存,可能导致内存不断膨胀。 5. 外部资源:例如数据库连接、文件流等,如果没有正确...

Global site tag (gtag.js) - Google Analytics