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

本文深入分析了Java中类的热替换、卸载以及Tomcat的热部署机制。通过自定义ClassLoader `HotSwapURLClassLoader` 实现类的热替换,展示了在程序运行时修改类文件并观察到类被重新加载的效果。同时讨论了类卸载的条件和限制,并给出`SimpleURLClassLoader`示例。在Tomcat中,热部署涉及到Context的卸载和重新加载,确保应用的更新。最后,指出了 PermGen space 溢出的问题及其原因。
摘要由CSDN通过智能技术生成

所以一个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 *@authorbanana14 */

15 public class HotSwapURLClassLoader extends URLClassLoader {

16     //缓存加载class文件的最后最新修改时间17     public static Map cacheLastModifyTimeMap = new HashMap();

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实例缓存下,是否已经加载过class51 //不同的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,即AppClassLoader69                 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                 //Ignore78             }

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 *@paramname91 *@paramcl92 *@return93 *@throwsClassNotFoundException94 */

95     public Class customLoad(String name,ClassLoader cl) throws ClassNotFoundException {

96         return customLoad(name, false,cl);

97     }

98

99     /**100 * 自定义加载101 *@paramname102 *@paramresolve103 *@return104 *@throwsClassNotFoundException105 */

106     public Class customLoad(String name, boolean resolve,ClassLoader cl)

107             throws ClassNotFoundException {

108         //findClass(

9b8a8a44dd1c74ae49c20a7cd451974e.png)调用的是URLClassLoader里面重载了ClassLoader的findClass(

9b8a8a44dd1c74ae49c20a7cd451974e.png)方法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 stub125         return super.findClass(name);

126     }

127

128     /**129 *@paramname130 *@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 *@paramname144 *@return145 */

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 *@paramname157 *@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 * 加载class38 */

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加载情况

dbb922ce6207fe9b4550686cbda28c18.png

总结:上述类热加载,需要自定义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实例缓存下,是否已经加载过class44         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,即AppClassLoader56                 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                 //Ignore65             }

66         }

67

68         return customLoad(name,this);

69     }

70

71     /**72 * 自定义加载73 *@paramname74 *@paramcl75 *@return76 *@throwsClassNotFoundException77 */

78     public Class customLoad(String name,ClassLoader cl) throws ClassNotFoundException {

79         return customLoad(name, false,cl);

80     }

81

82     /**83 * 自定义加载84 *@paramname85 *@paramresolve86 *@return87 *@throwsClassNotFoundException88 */

89     public Class customLoad(String name, boolean resolve,ClassLoader cl)

90             throws ClassNotFoundException {

91         //findClass(

9b8a8a44dd1c74ae49c20a7cd451974e.png)调用的是URLClassLoader里面重载了ClassLoader的findClass(

9b8a8a44dd1c74ae49c20a7cd451974e.png)方法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         //用自定义的加载器加载A8         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配置

097c40bfe020d39bd7656307388ac604.png

输出结果

.....

[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加载进了内存,也是可以被释放的。

图:程序运行中,引用没清楚前,内存中情况

f805e9c84b94dce91da4c1180cce423f.png图:垃圾回收后,程序没结束前,内存中情况 

121e362b72d5d8c5785cf955bc82fa1e.png

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加载资源的概况图:

b7d90023531072cbfeb150be31a16f69.png当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清除引用和资源8ffb731537a7da6d7ae2fd516b6a6f68.png

当app下面的class文件修改的时候,Tomcat更新步骤:

1 Context容器会有专门线程监控app下面的类的修改情况。

2 如果发现有类被修改了。那么调用Context.reload()。清楚一系列相关的引用和资源。

3 然后创新创建一个WebClassLoader实例,重新加载app下面需要的class。

图:Context清除引用和资源

24eb1e31c730aaf239051396e1acc6c4.png

在一个有一定规模的应用中,如果文件修改多次,重启多次的话,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类加载器 内存泄漏问题的探讨)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值