这篇文章主要是分析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文件。www.2cto.com
下面看一个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]
......