HotSwap和JRebel原理服务器
在2002年的时候,Sun在Java 1.4的JVM中引入了一种新的被称做HotSwap
的实验性技术,这一技术被合成到了Debugger API
内部,其容许调试者使用同一个类标识来更新类的字节码。这意味着全部对象均可以引用一个更新后的类,并在它们的方法被调用的时候执行新的代码,这就避免了不管什么时候只要有类的字节码被修改就要重载容器的这种要求。全部新式的IDE(包括Eclipse、IDEA和NetBeans)都支持这一技术,从Java 5开始,这一功能还经过Instrumentation API
直接提供给Java应用使用。框架
不幸的是,这种重定义仅限于修改方法体——除了方法体以外,它既不能添加方法或域,也不能修改其余任何东西。这限制了HotSwap的实用性,且其还因其余的一些问题而变得更糟:编辑器
Java编译器经常会建立合成的方法或是域,尽管你仅是修改了一个方法体(好比说,在添加一个类字面常量(class literal)、匿名的和内部的类的时候等等)。 在调试模式下运行经常会下降应用的速度或是引入其余的问题。分布式
这些状况致使了HotSwap不多被使用,较之应该可能被使用的频度要低。工具
自从引入了HotSwap以后,在最近的10年,这一问题已经被问了很是屡次。在支持作整组改变的JVM调用的bug中,这是一个得票率最高的bug ,但到目前为止,这一问题一直没有被落实。布局
一个声明:我不能说是一个JVM专家,我对JVM是如何实现的在整体上有着一个很好的理解,这几年来我有和少数几个(前)Sun工程师谈过,不过我并无验证我在这里说的每一件事情。不过话虽如此,对于这个bug依然处开发状态的缘由我确实是有一些想法的(不过若是你更清楚其中的缘由的话,欢迎指正)。性能
JVM是一种作了重度优化的软件,运行在多个平台上。性能和稳定性是其最高的优先事项。为了在不一样的环境中支持这些事项,Sun的JVM提供了这样的功能特点:优化
两个重度优化的即时编译器(-client和-server) 几个多代(multi-generational )垃圾收集器
这些功能特性使得类模式(schema)的发展变成了一个至关大的挑战。为了理解这其中的缘由,咱们须要稍微靠近一点看一看,究竟是须要用什么来支持方法和域的添加操做(甚至更深刻一些,修改继承的层次结构)。spa
在被加载到JVM中时,对象是由内存中的结构来表示的,结构占据了某个特定大小(它的域加上元数据)的连续的内存区域。为了添加一个域,咱们须要调整结构的大小,但由于临近的区域可能已被占用,咱们就须要把整个结构从新分配到一个不一样的区域中,这一区域中有足够可用的空间来把它填写进来。如今,因为咱们其实是更新了一个类(并不只是某个对象),因此咱们不得不对该类的每个对象都作这样的一件事。插件
这自己并不难实现——Java垃圾收集器就已是随时都在作重分配对象的工做的了。问题是,一个“堆”的抽象就仅是一个抽象而已。内存的实际布局取决于当前活动的垃圾收集器,并且,为了能与全部这些对象兼容,重分配应该有可能会被委派给活动的垃圾收集器。JVM在重分配期间还须要挂起,所以其在此期间同时进行GC工做也是合理的。
添加一个方法并不要求更新对象的结构,但确实是须要更新类的结构的,这也会体如今堆上。不过考虑一下这种状况:从类被载入以后的那一刻起,其从本质上来讲就是被永久冻结了的。这使得JIT(Just-In-Time)可以完成JVM执行的主要优化操做——内联。应用程序热点中的大多数方法调用会被取消,这些代码会被拷贝到对其作调用的方法中。一个简单的检测会被插进来,用以确保目标对象确实是咱们所认为的对象。
因而就有了这样好笑的事:在咱们可以添加方法到类中的时候,这种“简单的检查”是不够的。咱们须要的是一个至关复杂的检查,须要这样更复杂的检查来确保没有使用了相同名字的方法被添加到目标类以及目标类的超类中。另外,咱们也能够跟踪全部的内联点和它们的依赖,并在类被更新时,解除对它们所作的优化。两种方式可选择,或是付出性能方面的代价,或是带来更高的复杂性。
最重要的是,考虑到咱们正在讨论的是有着不一样的内存模型和指令集的多个平台,它们可能多多少少须要一些特定的处理,所以你给本身带来的是一个代价太高而没有太多投资回报的问题。
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与应用服务器整合在一块儿,当某个类或是资源被更新时,其被从工做区中而不是从归档文件中读入。
这一作法不只容许类的即时更新,且容许诸如HTML、XML、JSP、CSS、.properties等之类的任何类型的资源的即时更新。Maven用户甚至不须要建立一个rebel.xml文件,由于Maven插件会自动地生成该文件。
在消除周转期的这一过程当中,另外一个问题变得明显起来:现现在的应用已不只仅是类和资源,它们还经过大量的配置和元数据绑定在一块儿。当配置发生改变时,改变应该被反映到那个正在运行的应用上。然而,仅把对配置文件的修改变成是可见的是不够的,具体的框架必需要要重载配置,把改变反映到应用中才行。
为了在JRebel中支持这些类型的改变,咱们开发了一个开源的API ,该API容许咱们的团队和第三方的捐献者使用框架特有的插件来使用JRebel的功能特性,把配置中所作的改变传播到框架中。例如,咱们支持动态实时地在Spring中添加bean和依赖,以及支持在其余框架中所作的各类各样的改变。
本文总结了在未使用动态类加载器状况下的各类重载Java类的方法。咱们还讨论了致使HotSwap局限性的缘由,揭示了JRebel幕后的工做方式,以及讨论了在解决类重载问题时出现的其余问题。
原文地址:http://article.yeeyan.org/view/213582/186226