类加载器不喜欢我,想我慢下来

声明:正如我们读者所知,RebelLabs是从ZeroTurnaround的商业领域(i.e. JRebel & LiveRebel) 拆分出来的一个独立的品牌,但是在过去的几年里很多开发者再三询问我们的产品,譬如有人问:“你们的产品确实不错,但JRebel实际上是如何完成类加载的工作的呢?”我们也研究其他产品,比如Eclipse,IntelliJ,Jenkins,Maven等等,考虑到这一主题很高深,而且我们身边能找到这方面的专家只有Jevgeni Kabanov了(他是JRebel的创建者和ZeroTurnaround的首席执行官),所以我们决定把这些问题抛给他。下面让我们从类加载器的角度来看看JRebel的是如何工作的。希望你会觉得觉它很有趣!

一些背景:我们难道不能和谐相处吗?

自从二十世纪九十年代Java首次创建的时候,Java资源和类的加载就已经是一个问题了。通过增加启动和初始化的次数,Java应用程序服务器放大了这个问题。为了缓解这个这个问题,程序员们做了很多的努力,其中包括exploded deployment到应用程序服务器的方法,但它只是在非常小的程序中才会起作用,还有2001年创建的Java HotSwap。HotSwap启用时,可以让你已有的方法中立马让你的更改生效。一般它应用在调试session中,但是因为方法边界的限制它并不是总是有效的。构建、部署和重启经常要等上一段时间,甚至等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类型参数并且返回一个实际的Class对象。如果你以前使用类加载器那么它方法可能是你最熟悉的方法,因为它是日常编码中最常用的。defineClass 是JVM中一个final类型的方法,它需要一个从字节数组的参数,参数来自于一个文件或者在网络中的一个位置,并且产生相同的输出,一个Class对象。

一个类加载器也可以从classPath(类路径—一个环境变量)中找到资源。它和loadClass方法起作用的方式类似。有很多方法getResourcegetResources,他们返回一个URL或者是一个枚举类型的URLS,这些URL指向代表传递给方法的参数的名称的资源。

每一个类加载器都有父类;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方法。如果两个具有相同父类加载器的类加载器被要求去加载相同的类,它将只由父类进行一次。当两个类加载器分别的去分别加载相同的类的时候会变得很麻烦,因为这可能会导致问题,我们稍后再看看。

当一个J2EE应用程序服务器实现了一个J2EE规范的时候,他们中的一些更愿意把这个任务委派给父类,而其他则选择首先在WEB应用程序类加载中查找。让我们更加深入这一点,使用图1作为我们的例子。

在这个例子中,模块 WAR1 有它自己的类加载器而且更愿意去用它自己来加载类而不是委派给他的父类,这个类加载器被App1.ear限定了作用域。这意味着不同的WAR模块,像WAR1和WAR2一样,不能看见彼此的类。App1.ear 模块有他自己的类加载器并且它是WAR1和WAR2类加载器的父类。当WAR1和WAR2需要沿着层级层次向上发出请求时(也就是需要WAR类加载器作用域外的类时),App1.ear类加载器被WAR1和WAR2使用。当两个都存在的时候,WAR的类会覆盖EAR的类。最后EAR类加载器的父类是容器类加载器。EAR类加载器将委派请求发给容器类加载器,但是它不会像WAR类加载一样使用相同的方法,因为EAR类加载器实际上会向上委托请求而不是通过本地的类。正如您所看到的,这是相当难以理解的,和普通的JavaSE的类的加载行为是不一样的。

我们应该怎么重新加载应用程序中的类

通过先前我们查看的类加载器API我们知道,你只能装载类。也就是说,没有其他的方式去卸载或者重新加载这些类,所以为了在运行时重新创建一个类,你实际上必须抛弃整个类的层次结构然后重新创建它,这样才能加载新的类,并在运行的时候使用它,正如图2 展示的一样:

如果你已经有过一段的Java编程经历,你知道内存泄露经常会发生的。譬如集合含有指向对象的引用,本应该清除引用,但是没有清除。类加载器是一种很特殊的情况,不幸的是在Java平台的现状中,这些泄露既是难以避免的又是代价很大的:在几次重部署后,应用程序经常导致outofmemoryerror错误。

每一个对象都有一个指向它的类的引用,也就是有一个指向它的类加载器的引用。关键在于通过这个每一个类加载器都有一个指向它加载的每一个类的引用,这些类都拥有静态域,如图3。

这意味着:

1.如果一个类加载器内存泄露了,那么它会占用它加载的所有类和它们所有的静态域。静态域通常含有缓存、单例对象和不同的配置以及应用程序状态。即使你的应用程序没有一些大的静态缓存,这也不意味着你使用的框架不占用着它们(如Log4J是一种常见的罪魁祸首,因为它通常是放在服务器类路径中)。这就说明了为什么加载器泄露的代价会很大。

2.类加载器发生内存泄露很容易,只要类加载器加载了一个类,类创建了一个对象,然后给对象一个引用就可以了。就算对于一个看起来无害的对象(譬如没有域),但是它也会保留它的类加载器和所有应用程序的状态。就算在部署中没有出现问题,但是没有做适当的清理工作,这个也足够去埋下了泄露的隐患。在一个典型的应用程序将会有几个这样的地方,因为第三方库构建的原因,其中一些几乎不可能修复。因此,类加载器泄露是相当普遍的。

这就是存在的技术问题。为了更新我们在运行时的代码,我们通常需要建立,打包,重新部署,甚至重启容器看到更新的代码。接下来,我们将看看针对Java中这一核心问题的解决方法,包括在Java 1.4中引入的HotSwap类重载框架和JRebe的解决方法。

关于作者(SIMON MAPLE):

Simon是一个ZeroTurnaround的技术人员,他喜欢讨论和交流,不喜欢说教。他对技术社区投入了极大的热情,他既是伦敦Java社区组织(London Java Community, LJC)的成员,也是LJC JCP EC委员会的一员。Simon过去在IBM从事WebSphere Application Server项目的测试、开发以及技术教学的工作,一共超过了十年,那之后他加入了ZeroTurnaround。他喜欢看足球比赛(各种各样的足球),踢足球,喝茶还有陪伴家人。

原文链接: zeroturnaround 翻译: ImportNew.com - wudy
译文链接: http://www.importnew.com/11622.html
[ 转载请保留原文出处、译者和译文链接。]

关于作者: wudy

(新浪微博:@叼烟斗的纤夫

查看wudy的更多文章 >>



可能感兴趣的文章

发表评论

Comment form

(*) 表示必填项

4 条评论

  1. guizi 说道:

    作者虽然提出了当前classloader的不好地方,但是并没有提出更好的办法

    Thumb up 0 Thumb down 0

  2. 大菠萝 说道:

    没翻译完全么?

    Thumb up 0 Thumb down 0

跳到底部
返回顶部