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

2/10/2017来源:ASP.NET技巧人气:402

原文:http://www.blogjava.net/heavensay/archive/2012/11/07/389685.html

这篇文章主要是分析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,实现热替换的关键   1 package testjvm.testclassloader;   2    3 import java.io.File;   4 import java.io.FileNotFoundException;   5 import java.net.MalformedURLException;   6 import java.net.URL;   7 import java.net.URLClassLoader;   8 import java.util.HashMap;   9 import java.util.Map;  10   11 /**  12  * 只要功能是重新加载更改过的.class文件,达到热替换的作用  13  * @author banana  14  */  15 public class HotSwapURLClassLoader extends URLClassLoader {  16     //缓存加载class文件的最后最新修改时间  17     public static Map<String,Long> cacheLastModifyTimeMap = new HashMap<String,Long>();  18     //工程class类所在的路径  19     public static String PRojectClassPath = "D:/Ecpworkspace/ZJob-Note/bin/";  20     //所有的测试的类都在同一个包下  21     public static String packagePath = "testjvm/testclassloader/";  22       23     private static HotSwapURLClassLoader hcl = new HotSwapURLClassLoader();  24       25     public HotSwapURLClassLoader() {  26         //设置ClassLoader加载的路径  27         super(getMyURLs());  28     }  29       30     public static HotSwapURLClassLoader  getClassLoader(){  31         return hcl;  32     }   33   34     private static  URL[] getMyURLs(){  35         URL url = null;  36         try {  37             url = new File(projectClassPath).toURI().toURL();  38         } catch (MalformedURLException e) {  39             e.printStackTrace();  40         }  41         return new URL[] { url };  42     }  43       44     /**  45      * 重写loadClass,不采用双亲委托机制("java."开头的类还是会由系统默认ClassLoader加载)  46      */  47     @Override  48     public Class<?> loadClass(String name,boolean resolve) throws ClassNotFoundException {  49         Class clazz = null;  50         //查看HotSwapURLClassLoader实例缓存下,是否已经加载过class  51         //不同的HotSwapURLClassLoader实例是不共享缓存的  52         clazz = findLoadedClass(name);  53         if (clazz != null ) {  54             if (resolve){  55                 resolveClass(clazz);  56             }  57             //如果class类被修改过,则重新加载  58             if (isModify(name)) {  59                 hcl = new HotSwapURLClassLoader();  60                 clazz = customLoad(name, hcl);  61             }  62             return (clazz);  63         }  64   65         //如果类的包名为"java."开始,则有系统默认加载器AppClassLoader加载  66         if(name.startsWith("java.")){  67             try {  68                 //得到系统默认的加载cl,即AppClassLoader  69                 ClassLoader system = ClassLoader.getSystemClassLoader();  70                 clazz = system.loadClass(name);  71                 if (clazz != null) {  72                     if (resolve)  73                         resolveClass(clazz);  74                     return (clazz);  75                 }  76             } catch (ClassNotFoundException e) {  77                 // Ignore  78             }  79         }  80           81         return customLoad(name,this);  82     }  83   84     public Class load(String name) throws Exception{  85         return loadClass(name);  86     }  87   88     /**  89      * 自定义加载  90      * @param name  91      * @param cl   92      * @return  93      * @throws ClassNotFoundException  94      */  95     public Class customLoad(String name,ClassLoader cl) throws ClassNotFoundException {  96         return customLoad(name, false,cl);  97     }  98   99     /** 100      * 自定义加载 101      * @param name 102      * @param resolve 103      * @return 104      * @throws ClassNotFoundException 105      */ 106     public Class customLoad(String name, boolean resolve,ClassLoader cl) 107             throws ClassNotFoundException { 108         //findClass()调用的是URLClassLoader里面重载了ClassLoader的findClass()方法 109         Class clazz = ((HotSwapURLClassLoader)cl).findClass(name); 110         if (resolve) 111             ((HotSwapURLClassLoader)cl).resolveClass(clazz); 112         //缓存加载class文件的最后修改时间 113         long lastModifyTime = getClassLastModifyTime(name); 114         cacheLastModifyTimeMap.put(name,lastModifyTime); 115         return clazz; 116     } 117      118     public Class<?> loadClass(String name) throws ClassNotFoundException { 119         return loadClass(name,false); 120     } 121      122     @Override 123     protected Class<?> findClass(String name) throws ClassNotFoundException { 124         // TODO Auto-generated method stub 125         return super.findClass(name); 126     } 127      128     /** 129      * @param name 130      * @return .class文件最新的修改时间 131      */ 132     private long getClassLastModifyTime(String name){ 133         String path = getClassCompletePath(name); 134         File file = new File(path); 135         if(!file.exists()){ 136             throw new RuntimeException(new FileNotFoundException(name)); 137         } 138         return file.lastModified(); 139     } 140      141     /** 142      * 判断这个文件跟上次比是否修改过 143      * @param name 144      * @return 145      */ 146     private boolean isModify(String name){ 147         long lastmodify = getClassLastModifyTime(name); 148         long previousModifyTime = cacheLastModifyTimeMap.get(name); 149         if(lastmodify>previousModifyTime){ 150             return true; 151         } 152         return false; 153     } 154      155     /** 156      * @param name 157      * @return .class文件的完整路径 (e.g. E:/A.class) 158      */ 159     private String getClassCompletePath(String name){ 160         String simpleName = name.substring(name.lastIndexOf(".")+1); 161         return projectClassPath+packagePath+simpleName+".class"; 162     } 163      164 } 165  代码:Hot被用来修改的类 1 package testjvm.testclassloader; 2  3 public class Hot { 4     public void hot(){ 5         System.out.println(" version 1 : "+this.getClass().getClassLoader()); 6     } 7 } 8  代码:TestHotSwap测试类  1 package testjvm.testclassloader;  2   3 import java.lang.reflect.Method;  4   5 public class TestHotSwap {  6   7     public static void main(String[] args) throws Exception {  8         //开启线程,如果class文件有修改,就热替换  9         Thread t = new Thread(new MonitorHotSwap()); 10         t.start(); 11     } 12 } 13  14 class MonitorHotSwap implements Runnable { 15     // Hot就是用于修改,用来测试热加载 16     private String className = "testjvm.testclassloader.Hot"; 17     private Class hotClazz = null; 18     private HotSwapURLClassLoader hotSwapCL = null; 19  20     @Override 21     public void run() { 22         try { 23             while (true) { 24                 initLoad(); 25                 Object hot = hotClazz.newInstance(); 26                 Method m = hotClazz.getMethod("hot"); 27                 m.invoke(hot, null); //打印出相关信息 28                 // 每隔10秒重新加载一次 29                 Thread.sleep(10000); 30             } 31         } catch (Exception e) { 32             e.printStackTrace(); 33         } 34     } 35  36     /** 37      * 加载class 38      */ 39     void initLoad() throws Exception { 40         hotSwapCL = HotSwapURLClassLoader.getClassLoader(); 41         // 如果Hot类被修改了,那么会重新加载,hotClass也会返回新的 42         hotClazz = hotSwapCL.loadClass(className); 43     } 44 }      在测试类运行的时候,修改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加载情况      总结:上述类热加载,需要自定义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   1 package testjvm.testclassloader;   2    3 import java.io.File;   4 import java.net.MalformedURLException;   5 import java.net.URL;   6 import java.net.URLClassLoader;   7    8 public class SimpleURLClassLoader extends URLClassLoader {   9     //工程class类所在的路径  10     public static String projectClassPath = "E:/IDE/work_place/ZJob-Note/bin/";  11     //所有的测试的类都在同一个包下  12     public static String packagePath = "testjvm/testclassloader/";  13       14     public SimpleURLClassLoader() {  15         //设置ClassLoader加载的路径  16         super(getMyURLs());  17     }  18       19     private static  URL[] getMyURLs(){  20         URL url = null;  21         try {  22             url = new File(projectClassPath).toURI().toURL();  23         } catch (MalformedURLException e) {  24             e.printStackTrace();  25         }  26         return new URL[] { url };  27     }  28       29     public Class load(String name) throws Exception{  30         return loadClass(name);  31     }  32   33     public Class<?> loadClass(String name) throws ClassNotFoundException {  34         return loadClass(name,false);  35     }  36       37     /**  38      * 重写loadClass,不采用双亲委托机制("java."开头的类还是会由系统默认ClassLoader加载)  39      */  40     @Override  41     public Class<?> loadClass(String name,boolean resolve) throws ClassNotFoundException {  42         Class clazz = null;  43         //查看HotSwapURLClassLoader实例缓存下,是否已经加载过class  44         clazz = findLoadedClass(name);  45         if (clazz != null ) {  46             if (resolve){  47                 resolveClass(clazz);  48             }  49             return (clazz);  50         }  51   52         //如果类的包名为"java."开始,则有系统默认加载器AppClassLoader加载  53         if(name.startsWith("java.")){  54             try {  55                 //得到系统默认的加载cl,即AppClassLoader  56                 ClassLoader system = ClassLoader.getSystemClassLoader();  57                 clazz = system.loadClass(name);  58                 if (clazz != null) {  59                     if (resolve)  60                         resolveClass(clazz);  61                     return (clazz);  62                 }  63             } catch (ClassNotFoundException e) {  64                 // Ignore  65             }  66         }  67           68         return customLoad(name,this);  69     }  70   71     /**  72      * 自定义加载  73      * @param name  74      * @param cl   75      * @return  76      * @throws ClassNotFoundException  77      */  78     public Class customLoad(String name,ClassLoader cl) throws ClassNotFoundException {  79         return customLoad(name, false,cl);  80     }  81   82     /**  83      * 自定义加载  84      * @param name  85      * @param resolve  86      * @return  87      * @throws ClassNotFoundException  88      */  89     public Class customLoad(String name, boolean resolve,ClassLoader cl)  90             throws ClassNotFoundException {  91         //findClass()调用的是URLClassLoader里面重载了ClassLoader的findClass()方法  92         Class clazz = ((SimpleURLClassLoader)cl).findClass(name);  93         if (resolve)  94             ((SimpleURLClassLoader)cl).resolveClass(clazz);  95         return clazz;  96     }  97       98     @Override  99     protected Class<?> findClass(String name) throws ClassNotFoundException { 100         return super.findClass(name); 101     } 102 } 103  代码:A  1 public class A {   2 //  public static final Level CUSTOMLEVEL = new Level("test", 550) {}; // 内部类 3 } 代码:TestClassUnload,测试类  1 package testjvm.testclassloader;  2   3 public class TestClassUnLoad {  4   5     public static void main(String[] args) throws Exception {  6         SimpleURLClassLoader loader = new SimpleURLClassLoader();  7         // 用自定义的加载器加载A  8         Class clazzA = loader.load("testjvm.testclassloader.A");  9         Object a = clazzA.newInstance(); 10         // 清除相关引用 11         a = null; 12         clazzA = null; 13         loader = null; 14         // 执行一次gc垃圾回收 15         System.gc(); 16         System.out.println("GC over"); 17     } 18 } 19        运行的时候配置VM参数: -verbose:class;用于查看class的加载与卸载情况。如果用的是Eclipse,在Run Configurations中配置此参数即可。 图:Run Configurations配置    
输出结果 ..... [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加载进了内存,也是可以被释放的。 图:程序运行中,引用没清楚前,内存中情况 图:垃圾回收后,程序没结束前,内存中情况      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加载资源的概况图: 当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清除引用和资源 当app下面的class文件修改的时候,Tomcat更新步骤: 1 Context容器会有专门线程监控app下面的类的修改情况。 2 如果发现有类被修改了。那么调用Context.reload()。清楚一系列相关的引用和资源。 3 然后创新创建一个WebClassLoader实例,重新加载app下面需要的class。 图:Context清除引用和资源       在一个有一定规模的应用中,如果文件修改多次,重启多次的话,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体系结构)