天道不一定酬所有勤
但是,天道只酬勤

HotSwap和JRebel原理

HotSwap和JRebel原理

HotSwap和Instrumentation

在2002年的时候,Sun在Java 1.4的JVM中引入了一种新的被称作HotSwap的实验性技术,这一技术被合成到了Debugger API内部,其允许调试者使用同一个类标识来更新类的字节码。这意味着所有对象都可以引用一个更新后的类,并在它们的方法被调用的时候执行新的代码,这就避免了无论何时只要有类的字节码被修改就要重载容器的这种要求。所有新式的IDE(包括Eclipse、IDEA和NetBeans)都支持这一技术,从Java 5开始,这一功能还通过Instrumentation API直接提供给Java应用使用。

hotswap

不幸的是,这种重定义仅限于修改方法体——除了方法体之外,它既不能添加方法或域,也不能修改其他任何东西。这限制了HotSwap的实用性,且其还因其他的一些问题而变得更糟:

Java编译器常常会创建合成的方法或是域,尽管你仅是修改了一个方法体(比如说,在添加一个类字面常量(class literal)、匿名的和内部的类的时候等等)。 在调试模式下运行常常会降低应用的速度或是引入其他的问题。

这些情况导致了HotSwap很少被使用,较之应该可能被使用的频度要低。

为什么HotSwap仅限于对方法体起作用?

自从引入了HotSwap之后,在最近的10年,这一问题已经被问了非常多次。在支持做整组改变的JVM调用的bug中,这是一个得票率最高的bug ,但到目前为止,这一问题一直没有被落实。

一个声明:我不能说是一个JVM专家,我对JVM是如何实现的在总体上有着一个很好的理解,这几年来我有和少数几个(前)Sun工程师谈过,不过我并没有验证我在这里说的每一件事情。不过话虽如此,对于这个bug依然处开发状态的原因我确实是有一些想法的(不过如果你更清楚其中的原因的话,欢迎指正)。

JVM是一种做了重度优化的软件,运行在多个平台上。性能和稳定性是其最高的优先事项。为了在不同的环境中支持这些事项,Sun的JVM提供了这样的功能特色:

 两个重度优化的即时编译器(-client和-server)
 几个多代(multi-generational )垃圾收集器

这些功能特性使得类模式(schema)的发展变成了一个相当大的挑战。为了理解这其中的原因,我们需要稍微靠近一点看一看,到底是需要用什么来支持方法和域的添加操作(甚至更深入一些,修改继承的层次结构)。

在被加载到JVM中时,对象是由内存中的结构来表示的,结构占据了某个特定大小(它的域加上元数据)的连续的内存区域。为了添加一个域,我们需要调整结构的大小,但因为临近的区域可能已被占用,我们就需要把整个结构重新分配到一个不同的区域中,这一区域中有足够可用的空间来把它填写进来。现在,由于我们实际上是更新了一个类(并不仅是某个对象),所以我们不得不对该类的每一个对象都做这样的一件事。

这本身并不难实现——Java垃圾收集器就已经是随时都在做重分配对象的工作的了。问题是,一个“堆”的抽象就仅是一个抽象而已。内存的实际布局取决于当前活动的垃圾收集器,而且,为了能与所有这些对象兼容,重分配应该有可能会被委派给活动的垃圾收集器。JVM在重分配期间还需要挂起,因此其在此期间同时进行GC工作也是合理的。

添加一个方法并不要求更新对象的结构,但确实是需要更新类的结构的,这也会体现在堆上。不过考虑一下这种情况:从类被载入之后的那一刻起,其从本质上来说就是被永久冻结了的。这使得JIT(Just-In-Time)能够完成JVM执行的主要优化操作——内联。应用程序热点中的大多数方法调用会被取消,这些代码会被拷贝到对其做调用的方法中。一个简单的检测会被插进来,用以确保目标对象确实是我们所认为的对象。

于是就有了这样可笑的事:在我们能够添加方法到类中的时候,这种“简单的检查”是不够的。我们需要的是一个相当复杂的检查,需要这样更复杂的检查来确保没有使用了相同名字的方法被添加到目标类以及目标类的超类中。另外,我们也可以跟踪所有的内联点和它们的依赖,并在类被更新时,解除对它们所做的优化。两种方式可选择,或是付出性能方面的代价,或是带来更高的复杂性。

最重要的是,考虑到我们正在讨论的是有着不同的内存模型和指令集的多个平台,它们可能多多少少需要一些特定的处理,因此你给自己带来的是一个代价过高而没有太多投资回报的问题。

jrebel-agent

JRebel介绍

2007年,ZeroTurnaround宣布提供一种被称作JRebel(当时是JavaRebel)的工具,该工具可以在无需动态类加载器的情况下更新类,且只做极少的限制。不像HotSwap要依赖于IDE的集成,这一工具的工作方式是,监控磁盘上实际已编译的.class文件,无论何时只要有文件被更新就更新类。这意味着如果愿意的话,你可以把JRebel和文本编辑器、命令行的编译器放在一起使用。当然,它也被巧妙地整合到了Eclipse、InteliJ和NetBeans中。与动态的类加载器不一样,JRebel保留了所有现有的对象和类的标识和状态,允许开发者继续使用他们的应用而不会产生延迟。

如何使之生效?

对于初学者来说,JRebel工作在与HotSwap不同的一个抽象层面上。鉴于HotSwap是工作在虚拟机层面上,且依赖于JVM的内部运作,JRebel用到了JVM的两个显著的功能特征——抽象的字节码和类加载器。类加载器允许JRebel辨别出类被加载的时刻,然后实时地翻译字节码,用以在虚拟机和可执行代码之间创建另一个抽象层。

也有人使用这一功能特性来提供分析器、性能监控、后续(continuation)、软件事务性内存以及甚至是分布式的堆。 把字节码抽象和类加载器结合在一起,这是一种强大的组合,可被用来实现各种比类重载还要不寻常的功能。当我们越是深入地研究这一问题,我们就会看到面临的挑战并不仅是在类重载这件事上,而且是还要在性能和兼容性方面没有明显退化的情况下来做这件事情,

正如我们在Reloading Java Classes 101 一文中所做的回顾一样,重载类存在的问题是,一旦类被载入,它就不能被卸载或是改变;但是只要我们愿意,我们就可以自由地加载新的类。为了理解在理论上我们是如何重载类的,让我们来研究一下Java平台上的动态语言。具体来说,让我们先来看一看JRudy(我们做了许多的简化,以免对任何重要人物造成折磨)。

尽管JRuby以“类(class)”作为其功能特性,但在运行时,其每个对象都是动态的,任何时候都可以加入新的域和方法。这意味着JRuby对象与Map没有什么两样,有着从方法名字到方法实现的映射,以及域名到其值的映射。这些方法的实现被包含在匿名的类中,在遇到方法时这些类就会被生成。如果你添加了一个方法,则所有JRuby要做的事情就是生成一个新的匿名类,该类包含了这一方法的方法体。因为每个匿名类都有一个唯一的名称,因此在加载该类是不会有问题的,而这样做的结果是,应用被实时动态地更新了。

从理论上来说,由于字节码翻译通常是用来修改类的字节码,因此若仅仅是为了根据需要创建足够多的类来履行类的功能的话,我们没有什么理由不能使用类中的信息。这样的话,我们就可以使用如JRuby所做的相同转换来把所有的Java类分割成持有者类和方法体类。不幸的是,这样的一种做法会遭受(至少是)如下的问题:

性能。这样的设置将意味着,每个方法调用都会遭遇重定向。我们可以做优化,但应用程序的速度将会变慢至少一个数量级,内存的使用也会扶摇直上,因为有这么多的类被创建。 Java的SDK类。Java SDK中的类明显地比应用或是库中的类更加难以处理。此外它们通常会以本地的代码来实现,因此不能以“JRuby”的方式做转换。然而,如果我们让它们保持原样的话,那么就会引发各种的不兼容性错误,这些错误有可能是无法绕开的。 兼容性。尽管Java是一种静态的语言,但是它包含了一些动态的特性,比如说反射和动态代理等。如果我们采用了“JRuby”式的转换的话,这些功能特性就会失效,除非我们使用自己的类来替换掉Reflection API,而这些类知道这些要做的转换。

因此,JRebel并没有采用这样的做法。相反,其使用了一种更复杂的方法,基于先进的编译技术,留给我们一个主类和几个匿名的支持类,这些类由JIT的转换运行时做支持,其允许所进行的修改不会带来任何明显的性能或是兼容性的退化。它还

留有尽可能多完整的方法调用,这意味着JRebel把性能开销降低到了最小,使其轻量级化。
避免了改编(instrument)Java SDK,除了少数几个需要保持兼容性的地方外。
调整Reflection API的结果,这样我们就能够把这些结果中已添加/已删除的成员正确地包含进来。这也意味着注解(Annotation)的改变对于应用来说是可见的。

除了类重载之外——还有归档文件

重载类是一件Java开发者已经抱怨了很久的事情,不过一旦我们解决了它之后,另外的一些问题就随之而来了。

Java EE标准的制定并未怎么关注开发的周转期(Turnaround)(指的是从对代码做修改到观察到改变在应用中造成的影响这一过程所花费的时间)。其设想的是,所有的应用和它们的模块都被打包到归档文件(JAR、WAR和EAR)中,这意味着在能够更新应用中的任何文件之前,你需要更新归档文件——这通常是一个代价高昂的操作,涉及了诸如Ant或是Maven这一类的构建系统。正如我们在Reloading Java Classes 301 所做的讨论那样,可以通过使用展开式的开发和增量的IDE构建来尽量减少花销,不过对于大型的应用来说,这种做法通常不是一个可行的选择。

为了解决这一问题,在JRebel 2.x中,我们为用户开发了一种方式来把归档的应用和模块映射回到工作区中——用户在每个应用和模块中创建一个rebel.xml配置文件,该文件告诉JRebel在哪里可以找到源文件。JRebel与应用服务器整合在一起,当某个类或是资源被更新时,其被从工作区中而不是从归档文件中读入。

workspace-map

这一做法不仅允许类的即时更新,且允许诸如HTML、XML、JSP、CSS、.properties等之类的任何类型的资源的即时更新。Maven用户甚至不需要创建一个rebel.xml文件,因为Maven插件会自动地生成该文件。

除了类重载之外——还有配置和元数据

在消除周转期的这一过程中,另一个问题变得明显起来:现如今的应用已不仅仅是类和资源,它们还通过大量的配置和元数据绑定在一起。当配置发生改变时,改变应该被反映到那个正在运行的应用上。然而,仅把对配置文件的修改变成是可见的是不够的,具体的框架必须要要重载配置,把改变反映到应用中才行。

conf

为了在JRebel中支持这些类型的改变,我们开发了一个开源的API ,该API允许我们的团队和第三方的捐献者使用框架特有的插件来使用JRebel的功能特性,把配置中所做的改变传播到框架中。例如,我们支持动态实时地在Spring中添加bean和依赖,以及支持在其他框架中所做的各种各样的改变。

结论

本文总结了在未使用动态类加载器情况下的各种重载Java类的方法。我们还讨论了导致HotSwap局限性的原因,揭示了JRebel幕后的工作方式,以及讨论了在解决类重载问题时出现的其他问题。

原文地址:http://article.yeeyan.org/view/213582/186226

(全文完)
欢迎关注HollisChuang微信公众账号
打赏

如未加特殊说明,此网站文章均为原创,转载必须注明出处。HollisChuang's Blog » HotSwap和JRebel原理

分享到:更多 ()

HollisChuang's Blog

联系我关于我