JRebel 3.0 于不久前发布,我们采访了 Zeroturnaround 的 CTO Jevgeni Kabanov 以详细了解 JRebel 的一些内部工作原理、适用性以及集成问题,并了解 Java 开发者能从这一技术中获得什么益处。
Jevgeni 最近发表的一系列文章,详细讲解了JVM 类加载和运行时类替换的相关内容。本文中的部分内容是从他这一系列文章中提取出来的,另一部分则是我们交流的内容。
开发过程中重新部署或重起Java 应用都引入了不必要的时延,降低了生产效率。 JRebel 旨在通过给 JVM 增加一个处理运行时类替换的特殊javaagent 启动参数来显著缩短开发周期。
开发周期与生产效率
为了对生产效率的相关讨论提供支持,Zeroturnaround 收集了来自超过1100 个Java 开发者的统计数据。这些数据是公开的,用来计算平均小时等候时间。在平均小时等候时间中:有6 分钟用来构建的;而大约有10 分半是用来重新部署应用的。
除了直接等待时间外,还有其他隐性时间损耗。在软件开发这样的复杂任务环境中上下文切换也是个问题。有关开发流程的大量信息是保存在短期记忆中的,如果注意力被分散,要想恢复到原来的状态就得花大量的时间。诸如电话、email 及个人需求等工作中断所导致的上下文切换成本在现有文献中已有所讨论。不过前面所说的由基础设施所引起的上下文切换也适用于此。
由于工作期间的成绩更少,低工作效率也会导致开发者积极性降低。
有几种方法可以做到在不重新启动应用的情况下更新类,但每种方法都有其问题:
JVM HotSwap
或许有人会说 JVM Hotswapping 可以就地替换字节码,而且已经出现一段时间了(始于 2002 年,JDK 1.4)。但它有严格限制:它只能工作在Debug 模式下,而且只能处理方法体内的变化。它不允许结构的变化或增/ 删类成员。Hotswap 对classloader 也是一无所知,它只是通过类名来识别类,并且只能处理已存在的类。
从Java 5 开始,hotswapping 可通过 instrumentation API 来使用。JRebel 也使用了该 API,但仅仅用于底层类及类加载器,并没有在实际的重加载处理中运用。
JVM 的复杂性(尤其是垃圾回收、内存布局及 Hotspot)使得提供一个通用的透明类字节码替换方案比较困难。
其中一个问题是对象实例的状态必须不受影响,否则相关实例必须全部替换,对他们的所有引用也都要更新(包括级联变化)
应用服务器重部署(Redeployment)/ 热部署(Hotdeployment)
大多数 servlet 容器、应用服务器和 OSGi 的重部署特性实现思路都类似。
用专用类加载器(classloader)加载一个应用服务器(或其部分),如果你删除了对所有已加载类以及 classloader 的引用,你就能彻底卸载该应用并用一个新的 classloader 重新加载它。但是通常情况下,从 JVM 中彻底删除对所有实例、类或 classlodar 的引用是不可能的。
这样的话老应用仍部分存在,只是不再使用罢了。于是类加载器泄漏(classloader leaks)就会被引入,导致在连续重部署后内存消耗增加。
热部署可以有效地重启应用。为了恢复应用的状态,这些状态必须提前被保存并被重新应用或加载到“新”的应用或模块中。
当前基于组件的框架如 Grails 或 RIFE 都自己负责处理组件状态。这就是为什么用新 classloader 重新装载的组件在这些框架中不存在问题的原因。新组件的状态在框架中已经存在。组件的颗粒性也可以让很小的 bundle 被瞬时重新加载。
不过这种方式重新加载类存在一个普遍问题,即类的新老版本可能同时存在,框架必须小心处理这一问题。
JRebel
JRebel 原先叫 JavaRebel,自 2007 就有,它采取了一种不同的方式。当一个类被仪表化的(instrumented)类加载器加载时,在线创建了一个间接产物(indirection),其能够在重新加载时保持所有实例的身份和状态。该间接产物基于“高级编译技术(类似抽象字节码)”,会产生一个主类和几个匿名类、以及一些启用 JIT 的支持类。JRebel 让方法调用尽可能完整,以减少对性能的影响。基于同样的原因,它避免将 JDK 仪表化(instrumenting JDK)。为了给重加载类提供反射 API 支持,该 API 调用的结果也相应地被修改。
创建额外的类会导致多分配 20% 到 40% 的空间。按照 Jevgeni 的说法,较小的类及深层继承都会占用更多的内存。但是这是由内部实现引起的,你不应该依赖于它。
为了在一个打包的部署中重新加载类,JRebel 允许开发者将打包文件结构映射(通过 rebel.xml 配置文件)回开发空间(development workplace),以便让修改过的类以及所有其他资源得以重新加载。
由于 JRebel 对重载 / 刷新资源进行了全面综合考虑,因此也能够更新各种框架和容器的配置及元信息。为了能给不同框架都提供恰当的刷新设施,JRebel 提供了一个开源API 用以开发 JRebel plugins 。现有可用开源 plugin 中包含支持以下框架的 plugin:Guice、Spring、Tapestry 4、Struts 2、Wicked、Stripes、WebObjects。这些都是随发行包一起发行的。
上述三种不同类重加载方法的区别在zeroturnaround 网站上有相应的比较矩阵加以说明。
JRebel 3.0 改进之处
JRebel 3.0 版于 4 月 16 日发布,包含了不少重要改进。
- 性能提升,启动速度快两倍,内存使用减少 25%-30%
- 支持添加静态属性和修改 enums(自动初始化)
- 改进 EJB 支持及 plugins(尤其针对 IBM WebSphere、JBoss、Oracle Weblogic),支持 EJB 接口更新、依赖注入、对 EJB 3 支持更到位
- JPA —— 提供 OpenJPA 和 Hibernate plugins,能够反应实体、注解和配置变化(beta 版,默认禁用)
- 全面支持 JSP scriptlet,外部代码变化在 scriptlet 中立即生效
- 为 JSF 配置和注解变更提供 Mojarra plugin
- 更多的可用插件,支持:Spring、Guice、Seam、Weld Groovy、Wicket、Google App Engine、AspectJ、Struts 1 和 2
- 对诸如 Javassist 和 cglib(被许多其他工具所使用)这样的字节码框架提供特殊支持
按照 Jevgeni 的说法,最后一个问题比较棘手。当给类添加方法时,字节码框架的调用句柄接所收到了他们预期之外的合成方法调用。因此我们用一个redefineClass()
方法对 JRebel API 进行了扩展,该方法还允许重定义字节码代理。使用该 API,可以重写部分代理实现以允许动态类更新。现在有两个 plugins 专门支持这些框架。
还有一个遗留问题是 JDK 动态代理,由于他们对代理接口的限制因此很少需要集成。
JRebel 的目标是具备更多兼容性,以能够与任何基于 Java 的底层架构无缝集成。其中重要的一步是改进了与 EJB 的集成。
为了获得更好企业集成特性,Zeroturnaround 还针对老技术(Java 1.4、老的应用服务器版本、EJB 1.x/2.x)和集中许可证管理提供了 JRebel Enterprise Add-On 。该附加组件是作为免费更新提供给当前所有用户的。
许可和支持
JRebel 可在个人许可(USD 59)或商业许可(USD 149)下使用。你还可以免费试用30 天并有30 天的退款保证。
评论