JRebel实践 part2 behind the scene

Oct 26, 2010

这篇BLOG很大程度还得感谢Coach Jonathan ,我跑过去跟他讨论了一会,他说你可以继续在测测看看在controller层和repository层是否工作, 然后尝试着做一个关于这个的Session,让大家都了解了解,有很大可能会在这个项目中应用.他说了之后,我就开始着手查阅更多资料,然后直接促成了这篇文章,因为经验和认识的原因,里面有些观点可能不太准确. 实际上我们关心的是如何能在运行时实现重新装载类,而不需要去关闭整个程序.当整个程序跑起来了,特别指明WEB程序,每一次我改动了.java文件,然后编译,但是我想回到页面看到改动后的效果,但是我不想重复一下步骤 尽管这个流程可能只需要在命令行里敲一个简单的Ant命令,但是可能你需要重新登录,然后作一大堆敲击或者什么的,回到你上一次想看到变化的地方,这样一个重复量,就这样,时间,大把时间,白白流逝,更重要的是正如前一篇文章所言,节奏被打乱了. 所以我们想的是需要这样: 所以问题是这是怎么工作的?

答案是:class reloading.

什么是类重载了?
Whenever class bytecode was changed , all objects refered to an old class now refer to updated class and execute new code when their methods were called, preventing the need to reload a container or application.
也就是说它能允许你在运行时动态的改变某一个类的字节码,而且应用程序不需要重新启动却可以使用更新过得字节码的功能. 要理解这一句话,我们需要先大致了解Java中实例,类和类装载器之间的关系. 在Java中每一个对象都有对应这一样的一个class.这个class你可以简单的看成是拥有一系列方法的一个集合,它所有的方法的参数列表都是接受一个this.当它被加载到内存中时它会获得唯一的一个身份IDENTIY.这个类对象都是继承自java.lang.Class,所以你可以通过MyObject.getClass()来获得它的身份. 所以如果你调用myObject.method() 实际上JVM会间接的调用myObject.getClass().getDeclaredMethod("method).invoke(myObject). 那接下来就是ClassLoader拉,ClassLoader扮演着非常重要的角色,每一个Class对象都与之相关联,它的主要作用是定义Scope,也就是定义这个类是否可见.你可以认为每一个ClassLoader就相当于一个盒子,由这个ClassLoader加载的类,只有在这个盒子才可见,其他的ClassLoader不可见.所以这就给与你这样一种能力,由不同的Classloader来加载这个类更新后的版本. 所以判断两个类Class是否相同,除了名字之外,还需要考虑它所在的作用域. 所以你可以知道如果用两个Classloader来装载Class,但是其中一个装载较新的Class,你可以知道这两个CLass文件的Identity 是完全不相同的,问题就在于此时所有已有的对象都指向的是旧的那一个class,所以你无法看到更新后的Class文件的被调用. 举一个例子,假设你有一个手机,然后你把想换新号码,那么会有两个步骤:第一,讲原有SIM卡上已存的号码拷贝到新的SIM卡上;第二步:挨个通知你的朋友,叫他们更新他们的手机上你的号码. 所以这种事情如果要应用到对象和类上面,操作起来,就是首先拷贝原来实例的状态,然后开始将所以引用到原来Class上的指针指向新的Class上面. Sun怎么处理? SUN提出了HotSwap的概念,什么是HotSwap? HotSwap:简单的说,就是允许你在Debug模式下从IDE中修改更新类的字节码,但是使用一样的类标志.也就是说无论什么时候类的字节码变化的时候,所有对象都能引用到更新过的类然后执行新的代码,而不需要去重新转载整个容器.Java5 已经加入这样的支持,你也可以使用Instrumentation API来实现. 以下是Instrument 但是它有非常显著的不足,严重的影响了它的应用: 1.它只能修改方法的内容,而不能对增加方法或者字段其作用. 2.只能运行在Debug模式下会使整个应用程序变慢并可能带来其他问题. 在引出JRebel的办法之前,我们需要在引入下一个概念HotDeploy 什么是HotDeploy? 在某些WEB容器中通常有一个特殊的目录,比如在Tomcat和Jetty中的webapps目录,而这些容器 会周期性的扫描这个目录查看是否有新的web程序或者已有的程序有了修改.如果有,那么它会自动触发部署. HotDeploy在某种程度上减少了开发者的手动干预,但是从本质上没有很大的提高.什么本质了?就是它仍然会 做出部署,所谓的"重新部署",实际上它会将原来的丢掉原来的Classloader,并且销毁所有其中的class,具体点 就是包含其中Servlets, 然后会创建一个新的classloader,重新装载Servlets,并在其上面调用初始方法. 为什么更详细点,这里将简要介绍一下J2EE常见的classloader结构,这里以Tomcat容器为例. 那你可以看到每一个WebApplicationLoader负责一个Web应用程序,更具体点,它负责装载你在WEB-INF/classes和WEB-INF/lib下的文件.所以每一次TOMCAT或者JETTY进行热部署,实际上它会跑掉原来的WebApplicationLoader,然后创建一个新的WebApplicationLoader,然后重新装载所有的类,这样的问题之一是所以(应该是大部分)的状态都要让用户重新来创建. 这是一个小问题,差不多你就是多输几次密码,或者多点击几次页面等等,这是可以控制的,但是你没法控制却是OutOfMemory. 为什么会在一个带有热部署支持的容器中容易发生这样的事情? 问题就在于每次热部署都回抛掉原有的classloader,然后创建新的classloader, 如果每次创建的代价不大,也就是每一次需要重新装载的类不是很多,那可能问题不是那么明显.但是如果热部署比较平凡,而且每一次classloader需要装载的类或资源非常多,对于一个WEB项目,这是非常有可能的,可能几次过后,你的资源或者内存就会被挤爆,也就会跑出OutOfMemory异常,而这种异常是非常intricate,难以追踪的(你可以使用一些Profile的工具,如jhat来查看堆中的信息). 所以JRebel怎么去解决这个问题了? 在看看JRebel的处理,它通过引入-javaagent来在classloader级别进行操作.什么是-javaagent? 简要的来说,就是能允许你在classloader链接之后在装载之前做一些字节码上的操作,比如常见的日志记录功能(你不用在写代码时候去给每个类都去写,可以在装载之前时修改它的字节码给每一个类都添加日志记录声明等等),还有一些AOP的实现. 基于我现在的知识,我还不能给出一个非常明确的解释,也希望了解的能给点提示. JRebel正是借用这个功能,在装载之前,继承并扩展了WebApplicationClassLoader,每一次重新装载都不会创建新的CLASSLOADER 而是重用这样一个扩展的classloader来装载,至于怎么样的,这个.....在我知识范围之外,不过那个Jevgeni Kabanov 在这篇博客Reloading Java Classes 401: HotSwap and JRebel — Behind the Scenes中有讲到这个话题时,以JRuby作为比较,讲了它是怎么弄的,但是我这个....似懂非懂,其实还是没搞不懂. JRebel跟Hotswap不同的是它还支持其他的修改,比如增加方法等等,更贴近于现实生活中的需要. 下面是引用自JavaRebel says no to restart– Really??的关于两者对比的图: 同时这篇文章还例举出了JR的一些限制和不足的地方,有兴趣的可以看看,但是这篇文章是2008年的,我不太清楚现在是否有解决,我现在也没有充分的实验过,所以不过我发现一个问题是: 当我改动文件跑出一个异常之后,跳到页面,页面提示错误,这当然是我想要的.但是如果我返回到代码,去掉这些异常,然后返回到页面,却发现没有一个页面能工作,全部打不开.诡异的是,过了个半分钟样子,它要可以了. 不知道是不是页面缓存的问题,有待研究下. 下面是一些引用和有趣的链接: 这次SESSION的PPT: JRebel on Chronicles (SlideShare搞了半天没传上去,压成ZIP也传不上) Jevgeni Kabanov 关于JRebel工作原理介绍的一系列blog: Reloading Java Classes 101: Objects, Classes and ClassLoaders 介绍classloading机制的文章:Internals of Java Class Loading Tomcat类结构的文章:Catalina Class Loader Hierarchy JRebel和HotSwap比较: JavaRebel says no to restart– Really??