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

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

阅读更多

从上个世纪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
分享到:
评论

相关推荐

    Tomcat 检测内存泄漏实例详解

    这个要从热部署开始说起,因为tomcat提供了不必重启容器而只需重启web应用以达到热部署的功能,其实现是通过定义一个WebappClassLoader类加载器,当热部署时就将原来的类加载器废弃并重新实例化一个WebappCl

    如何用Java编写一段代码引发内存泄露

    文本来自StackOverflow问答网站的一个...  线程通过某个类加载器(可以自定义)加载一个类。  该类分配了大块内存(比如new byte[1000000]),在某个静态变量存储一个强引用,然后在ThreadLocal中存储它自身的引用

    内存管理内存管理内存管理

    由于这类地址不必反映内存所在的物理位置,所以它们被称为虚拟内存。操作系统维持着一个虚拟地址到物理地址的转换的表,以便计算机硬件可以正确地响应地址请求。并且,如果地址在硬盘上而不是在 RAM 中,那么...

    深入理解Java虚拟机精华知识点

    Java虚拟机(JVM)是Java Virtual Machine的缩写,...类加载器负责将字节码文件加载到内存中,运行时数据区用于存储程序执行时所需的数据,执行引擎则负责执行字节码文件,而垃圾收集器则负责回收不再使用的内存空间。

    操作系统(内存管理)

    由于这类地址不必反映内存所在的物理位置,所以它们被称为虚拟内存。操作系统维持着一个虚拟地址到物理地址的转换的表,以便计算机硬件可以正确地响应地址请求。并且,如果地址在硬盘上而不是在 RAM 中,那么操作...

    JVM面试专题

    你使⽤过哪些或者你在什么场景下需要⼀个⾃ 定义的类加载器吗? 7、描述一下JVM加载class文件的原理机制? 8、Java对象创建过程 9、类的生命周期【加载过程】 10、Java 中会存在内存泄漏吗,请简单描述。 11、GC是...

    2023年丰富的Java源码学习资料.pdf

    5. 安全性:Java在设计时注重安全性,提供了诸如类加载器、安全管理器和异常处理机制等功能来保护应用程序免受潜在的安全漏洞和恶意攻击。 总之,Java是一种功能强大、可靠性高、跨平台性强的编程语言,广泛应用于...

    涵盖了90%以上的面试题

    为什么要自定义类加载器 如何自定义类加载器 什么是GC 内存泄漏和内存溢出 Java的内存模型(JVM的内存划分) JVM内存模型1.7和1.8的区别 如何判断一个对象是否是垃圾对象 垃圾回收算法 Minor GC和Full GC 垃圾收集器 ...

    Java后端面试问题整理.docx

    • 熟悉常用IO模型(BIO、NIO、AIO),熟悉JVM类加载过程与机制 • 了解JVM性能监控以及调优,会使用jps、jstack、jmap、jstat、jhat,了解内存泄露排查具体方法 • Java基础 • 熟练的使用Java语言进行面向对象程序...

    java 面试题 总结

    新类继承了原始类的特性,新类称为原始类的派生类(子类),而原始类称为新类的基类(父类)。派生类可以从它的基类那里继承方法和实例变量,并且类可以修改或增加新的方法使之更适合特殊的需要。 3.封装: 封装是把...

    JavaSE基础面试题.docx

    2.论述类的加载机制 3.对于反射的理解 4.GC是什么?为什么要有GC 5.heap(堆)和stack(栈)的区别 6.内存泄漏和内存溢出 7.垃圾回收器的优点和原理,并考虑2中回收机制 8.加速垃圾回收的方式 9.JVM生命周期及体系...

    Java常见面试问题整理.docx

    在1.8之后,由于永久代内存经常不够用或发生内存泄露,爆出异常OOM,所以在1.8之后废弃永久代,引入元空间的概念。元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。 直接内存:不受JVM ...

    java7源码-test1:测试1

    8、不同类加载器的例子。 9、try-with-resource语句的例子--TryWithResource类. 10、利用反射获取构造方法的例子--ReflectTest类. 11、动态代理的例子,以及网上动态代理的例子--com.cn21.invocation包。 12、实现...

    编程高手箴言(推荐)

    3.4.3 线程的内存泄漏的主要原因 96 3.4.4 进程管理 98 3.4.5 同步机制 100 3.5 PE结构分析 103 3.5.1 PE头标 103 3.5.2 表节 113 3.5.3 PE文件引入 119 3.5.4 PE文件引出 125 3.5.5 PE文件资源 129 第4章 编程语言...

    Android开发艺术探索.任玉刚(带详细书签).pdf

    本书是一本Android进阶类书籍,采用理论、源码和实践相结合的方式来阐述高水准的Android应用开发要点。本书从三个方面来组织内容。... 15.2 内存泄露分析之MAT工具 502 15.3 提高程序的可维护性 506

    Android典型技术模块开发详解

    16.2.4 如何避免内存泄漏 16.3 ActivityGroup 16.4 ViewStub 16.5 Bitmap内存溢出 16.5.1 图片预先缩放 16.5.2 普通的图片缩放方法 16.5.3 Dalvik虚拟机的堆内存分配 16.5.4 Bitmap对象及时释放 16.6 多分辨率适应 ...

    编程高手箴言(中文完整版)(13M)

    3.4.3 线程的内存泄漏的主要原因 96 3.4.4 进程管理 98 3.4.5 同步机制 100 3.5 PE结构分析 103 3.5.1 PE头标 103 3.5.2 表节 113 3.5.3 PE文件引入 119 3.5.4 PE文件引出 125 3.5.5 PE文件资源 129 第4章 编程语言...

Global site tag (gtag.js) - Google Analytics