【转】class卸载、热替换和Tomcat的热部署的分析

这篇文章主要是分析Tomcat中关于热部署和JSP更新替换的原理,在此之前先介绍class的热替换和class的卸载的原理。

一 class的热替换
ClassLoader中重要的方法
loadClass
      ClassLoader.loadClass(...) 是ClassLoader的入口点。当一个类没有指明用什么加载器加载的时候,JVM默认采用AppClassLoader加载器加载没有加载过的class,调用的方法的入口就是loadClass(...)。如果一个class被自定义的ClassLoader加载,那么JVM也会调用这个自定义的ClassLoader.loadClass(...)方法来加载class内部引用的一些别的class文件。重载这个方法,能实现自定义加载class的方式,抛弃双亲委托机制,但是即使不采用双亲委托机制,比如java.lang包中的相关类还是不能自定义一个同名的类来代替,主要因为JVM解析、验证class的时候,会进行相关判断。
 
defineClass
      系统自带的ClassLoader,默认加载程序的是AppClassLoader,ClassLoader加载一个class,最终调用的是defineClass(...)方法,这时候就在想是否可以重复调用defineClass(...)方法加载同一个类(或者修改过),最后发现调用多次的话会有相关错误:
...
java.lang.LinkageError 
attempted duplicate class definition
...
所以一个class被一个ClassLoader实例加载过的话,就不能再被这个ClassLoader实例再次加载(这里的加载指的是,调用了defileClass(...)放方法,重新加载字节码、解析、验证。)。而系统默认的AppClassLoader加载器,他们内部会缓存加载过的class,重新加载的话,就直接取缓存。所与对于热加载的话,只能重新创建一个ClassLoader,然后再去加载已经被加载过的class文件。

下面看一个class热加载的例子:
代码:HotSwapURLClassLoader自定义classloader,实现热替换的关键
                                          Class clazz = ((HotSwapURLClassLoader)cl).findClass(name);
                      ((HotSwapURLClassLoader)cl).resolveClass(clazz);
                           cacheLastModifyTimeMap.put(name,lastModifyTime);
              }
     
                   }
     
     @Override
                            }
     
                      String path = getClassCompletePath(name);
         File file =                                }
              }
     
                                                               }
              }
     
                      String simpleName = name.substring(name.lastIndexOf(".")+1);
              }
     
 }
 

代码:Hot被用来修改的类

  
               System.out.println(" version 1 : "+     }
 }
 

代码:TestHotSwap测试类

         t.start();
     }
 }
 
                      
     @Override
                                            initLoad();
                 Object hot = hotClazz.newInstance();
                 Method m = hotClazz.getMethod("hot");
                 m.invoke(hot,                   Thread.sleep(10000);
             }
         }              e.printStackTrace();
         }
     }
 
                     hotSwapCL = HotSwapURLClassLoader.getClassLoader();
                  hotClazz = hotSwapCL.loadClass(className);
     }
 }

在测试类运行的时候,修改Hot.class文件 
Hot.class

原来第五行:System.out.println(" version 1 : "+this.getClass().getClassLoader());
改后第五行:System.out.println(" version 2 : "+this.getClass().getClassLoader());

输出

 version 1 : testjvm.testclassloader.HotSwapURLClassLoader@610f7612
 version 1 : testjvm.testclassloader.HotSwapURLClassLoader@610f7612
 version 2 : testjvm.testclassloader.HotSwapURLClassLoader@45e4d960
 version 2 : testjvm.testclassloader.HotSwapURLClassLoader@45e4d960

所以HotSwapURLClassLoader是重加载了Hot类 。注意上面,其实当加载修改后的Hot时,HotSwapURLClassLoader实例跟加载没修改Hot的HotSwapURLClassLoader不是同一个。
图:HotSwapURLClassLoader加载情况
【转】class卸载、热替换和Tomcat的热部署的分析
     总结:上述类热加载,需要自定义ClassLoader,并且只能重新实例化ClassLoader实例,利用新的ClassLoader实例才能重新加载之前被加载过的class。并且程序需要模块化,才能利用这种热加载方式。

二 class卸载
      在Java中class也是可以unload。JVM中class和Meta信息存放在PermGen space区域。如果加载的class文件很多,那么可能导致PermGen space区域空间溢出。引起:java.lang.OutOfMemoryErrorPermGen space.  对于有些Class我们可能只需要使用一次,就不再需要了,也可能我们修改了class文件,我们需要重新加载 newclass,那么oldclass就不再需要了。那么JVM怎么样才能卸载Class呢。

      JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载(unload):

- 该类所有的实例都已经被GC。
   - 加载该类的ClassLoader实例已经被GC。
   - 该类的java.lang.Class对象没有在任何地方被引用。

GC的时机我们是不可控的,那么同样的我们对于Class的卸载也是不可控的。 

例子:
代码:SimpleURLClassLoader,一个简单的自定义classloader

              }
 }
 

代码:A

   }

代码:TestClassUnload,测试类

                  a =          clazzA =          loader =                   System.gc();
         System.out.println("GC over");
     }
 }
 

运行的时候配置VM参数: -verbose:class;用于查看class的加载与卸载情况。如果用的是Eclipse,在Run Configurations中配置此参数即可。
图:Run Configurations配置    
【转】class卸载、热替换和Tomcat的热部署的分析

输出结果

.....
[Loaded java.net.URI$Parser from E:\java\jdk1.7.0_03\jre\lib\rt.jar]
[Loaded testjvm.testclassloader.A from file:/E:/IDE/work_place/ZJob-Note/bin/]
[Unloading class testjvm.testclassloader.A]
GC over
[Loaded sun.misc.Cleaner from E:\java\jdk1.7.0_03\jre\lib\rt.jar]
[Loaded java.lang.Shutdown from E:\java\jdk1.7.0_03\jre\lib\rt.jar]
......

上面输出结果中的确A.class被加载了,然后A.class又被卸载了。这个例子中说明了,即便是class加载进了内存,也是可以被释放的。

图:程序运行中,引用没清楚前,内存中情况
【转】class卸载、热替换和Tomcat的热部署的分析
图:垃圾回收后,程序没结束前,内存中情况
 
【转】class卸载、热替换和Tomcat的热部署的分析

    1、有启动类加载器加载的类型在整个运行期间是不可能被卸载的(jvm和jls规范).
    2、被系统类加载器和标准扩展类加载器加载的类型在运行期间不太可能被卸载,因为系统类加载器实例或者标准扩展类的实例基本上在整个运行期间总能直接或者间接的访问的到,其达到unreachable的可能性极小.(当然,在虚拟机快退出的时候可以,因为不管ClassLoader实例或者Class(java.lang.Class)实例也都是在堆中存在,同样遵循垃圾收集的规则).
    3、被开发者自定义的类加载器实例加载的类型只有在很简单的上下文环境中才能被卸载,而且一般还要借助于强制调用虚拟机的垃圾收集功能才可以做到.可以预想,稍微复杂点的应用场景中(尤其很多时候,用户在开发自定义类加载器实例的时候采用缓存的策略以提高系统性能),被加载的类型在运行期间也是几乎不太可能被卸载的(至少卸载的时间是不确定的).
      综合以上三点, 一个已经加载的类型被卸载的几率很小至少被卸载的时间是不确定的.同时,我们可以看的出来,开发者在开发代码时候,不应该对虚拟机的类型卸载做任何假设的前提下来实现系统中的特定功能.

三 Tomcat中关于类的加载与卸载
        Tomcat中与其说有热加载,还不如说是热部署来的准确些。因为对于一个应用,其中class文件被修改过,那么Tomcat会先卸载这个应用(Context),然后重新加载这个应用,其中关键就在于自定义ClassLoader的应用。这里有篇文章很好的介绍了tomcat中对于ClassLoader的应用,请点击here

Tomcat启动的时候,ClassLoader加载的流程:
1 Tomcat启动的时候,用system classloader即AppClassLoader加载{catalina.home}/bin里面的jar包,也就是tomcat启动相关的jar包。
2 Tomcat启动类Bootstrap中有3个classloader属性,catalinaLoader、commonLoader、sharedLoader在Tomcat7中默认他们初始化都为同一个StandardClassLoader实例。具体的也可以在{catalina.home}/bin/bootstrap.jar包中的catalina.properites中进行配置。
3 StandardClassLoader加载{catalina.home}/lib下面的所有Tomcat用到的jar包。
4 一个Context容器,代表了一个app应用。Context-->WebappLoader-->WebClassLoader。并且Thread.contextClassLoader=WebClassLoader。应用程序中的jsp文件、class类、lib/*.jar包,都是WebClassLoader加载的。

Tomcat加载资源的概况图:
【转】class卸载、热替换和Tomcat的热部署的分析

当Jsp文件修改的时候,Tomcat更新步骤:
1 但访问1.jsp的时候,1.jsp的包装类JspServletWrapper会去比较1.jsp文件最新修改时间和上次的修改时间,以此判断1.jsp是否修改过。
2 1.jsp修改过的话,那么jspservletWrapper会清除相关引用,包括1.jsp编译后的servlet实例和加载这个servlet的JasperLoader实例。
3 重新创建一个JasperLoader实例,重新加载修改过后的1.jsp,重新生成一个Servlet实例。
4 返回修改后的1.jsp内容给用户。
图:Jsp清除引用和资源

【转】class卸载、热替换和Tomcat的热部署的分析

当app下面的class文件修改的时候,Tomcat更新步骤:
1 Context容器会有专门线程监控app下面的类的修改情况。
2 如果发现有类被修改了。那么调用Context.reload()。清楚一系列相关的引用和资源。
3 然后创新创建一个WebClassLoader实例,重新加载app下面需要的class。
图:Context清除引用和资源 
【转】class卸载、热替换和Tomcat的热部署的分析
     在一个有一定规模的应用中,如果文件修改多次,重启多次的话,java.lang.OutOfMemoryErrorPermGen space这个错误的的出现非常频繁。主要就是因为每次重启重新加载大量的class,超过了PermGen space设置的大小。两种情况可能导致PermGen space溢出。一、GC(Garbage Collection)在主程序运行期对PermGen space没有进行清理(GC的不可控行),二、重启之前WebClassLoader加载的class在别的地方还存在着引用。这里有篇很好的文章介绍了class内存泄露-here

参考:
http://blog.csdn.net/runanli/article/details/2972361(关于Class类加载器 内存泄漏问题的探讨)

http://www.blogjava.net/zhuxing/archive/2008/07/24/217285.html(Java虚拟机类型卸载和类型更新解析)

http://www.ibm.com/developerworks/cn/java/j-lo-hotswapcls/(Java 类的热替换 —— 概念、设计与实现)
http://www.iteye.com/topic/136427(classloader体系结构)

上一篇:基于ACE的c++线程封装


下一篇:python之路 - 基础3