概述

名词解释:所谓热部署,就是在应用正在运行的时候升级软件,却不需要重新启动应用。本文主要是分析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(…)方法加载同一个类(或者修改过),最后发现调用多次的话会有相关错误:

1
2
java.lang.LinkageError
attempted duplicate class definition

所以一个class被一个ClassLoader实例加载过的话,就不能再被这个ClassLoader实例再次加载(这里的加载指的是,调用了defileClass(...)方法,重新加载字节码、解析、验证)。而系统默认的AppClassLoader加载器,他们内部会缓存加载过的class,重新加载的话,就直接取缓存。所与对于热加载的话,只能重新创建一个ClassLoader,然后再去加载已经被加载过的class文件

下面看一个class热加载的例子: 代码:HotSwapURLClassLoader自定义classloader,实现热替换的关键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
package testjvm.testclassloader;
import java.io.File;
import java.io.FileNotFoundException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.Map;
/**
  * 只要功能是重新加载更改过的.class文件,达到热替换的作用
  * @author banana
  */
public class HotSwapURLClassLoader extends URLClassLoader {
    //缓存加载class文件的最后最新修改时间
    public static Map<String,Long> cacheLastModifyTimeMap = new HashMap<String,Long>();
    //工程class类所在的路径
    public static String projectClassPath = "D:/Ecpworkspace/ZJob-Note/bin/";
    //所有的测试的类都在同一个包下
    public static String packagePath = "testjvm/testclassloader/";
    private static HotSwapURLClassLoader hcl = new HotSwapURLClassLoader();
    public HotSwapURLClassLoader() {
        //设置ClassLoader加载的路径
        super(getMyURLs());
    }
    public static HotSwapURLClassLoader  getClassLoader(){
        return hcl;
    }
    private static  URL[] getMyURLs(){
        URL url = null;
        try {
            url = new File(projectClassPath).toURI().toURL();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
            return new URL[] { url };
    }
    /**
     * 重写loadClass,不采用双亲委托机制("java."开头的类还是会由系统默认ClassLoader加载)
     */
    @Override
    public Class<?> loadClass(String name,boolean resolve) throws ClassNotFoundException {
        Class clazz = null;
        //查看HotSwapURLClassLoader实例缓存下,是否已经加载过class
        //不同的HotSwapURLClassLoader实例是不共享缓存的
        clazz = findLoadedClass(name);
        if (clazz != null ) {
            if (resolve){
                resolveClass(clazz);
            }
            //如果class类被修改过,则重新加载
            if (isModify(name)) {
                hcl = new HotSwapURLClassLoader();
                clazz = customLoad(name, hcl);
            }
            return (clazz);
        }
        //如果类的包名为"java."开始,则有系统默认加载器AppClassLoader加载
        if(name.startsWith("java.")){
            try {
                //得到系统默认的加载cl,即AppClassLoader
                ClassLoader system = ClassLoader.getSystemClassLoader();
                clazz = system.loadClass(name);
                if (clazz != null) {
                    if (resolve)
                        resolveClass(clazz);
                   return (clazz);
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }
        }
        return customLoad(name,this);
     }
     public Class load(String name) throws Exception{
         return loadClass(name);
     }
     /**
      * 自定义加载
      * @param name
      * @param cl
      * @return
      * @throws ClassNotFoundException
      */
     public Class customLoad(String name,ClassLoader cl) throws ClassNotFoundException {
         return customLoad(name, false,cl);
     }
     /**
      * 自定义加载
      * @param name
      * @param resolve
     * @return
      * @throws ClassNotFoundException
      */
     public Class customLoad(String name, boolean resolve,ClassLoader cl) throws ClassNotFoundException {
         //findClass()调用的是URLClassLoader里面重载了ClassLoader的findClass()方法
         Class clazz = ((HotSwapURLClassLoader)cl).findClass(name);
         if (resolve)
             ((HotSwapURLClassLoader)cl).resolveClass(clazz);
         //缓存加载class文件的最后修改时间
         long lastModifyTime = getClassLastModifyTime(name);
         cacheLastModifyTimeMap.put(name,lastModifyTime);
         return clazz;
     }
     public Class<?> loadClass(String name) throws ClassNotFoundException {
         return loadClass(name,false);
     }
     @Override
     protected Class<?> findClass(String name) throws ClassNotFoundException {
         // TODO Auto-generated method stub
        return super.findClass(name);
     }
     /**
      * @param name
      * @return .class文件最新的修改时间
      */
     private long getClassLastModifyTime(String name){
         String path = getClassCompletePath(name);
         File file = new File(path);
         if(!file.exists()){
            throw new RuntimeException(new FileNotFoundException(name));
         }
         return file.lastModified();
     }
     /**
      * 判断这个文件跟上次比是否修改过
      * @param name
      * @return
      */
     private boolean isModify(String name){
        long lastmodify = getClassLastModifyTime(name);
        long previousModifyTime = cacheLastModifyTimeMap.get(name);
        if(lastmodify>previousModifyTime){
            return true;
       }
       return false;
     }
    /**
     * @param name
     * @return .class文件的完整路径 (e.g. E:/A.class)
     */
     private String getClassCompletePath(String name){
         String simpleName = name.substring(name.lastIndexOf(".")+1);
         return projectClassPath+packagePath+simpleName+".class";
     }   
 }

代码:Hot被用来修改的类

1
2
3
4
5
6
package testjvm.testclassloader;
public class Hot {
    public void hot(){
        System.out.println(" version 1 : "+this.getClass().getClassLoader());
    }
}

代码:TestHotSwap测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package testjvm.testclassloader;
import java.lang.reflect.Method;
public class TestHotSwap {
    public static void main(String[] args) throws Exception {
        //开启线程,如果class文件有修改,就热替换
        Thread t = new Thread(new MonitorHotSwap());
        t.start();
    }
}
public class MonitorHotSwap implements Runnable {
    // Hot就是用于修改,用来测试热加载
    private String className = "testjvm.testclassloader.Hot";
    private Class hotClazz = null;
    private HotSwapURLClassLoader hotSwapCL = null;
    @Override
    public void run() {
        try {
            while (true) {
                initLoad();
                Object hot = hotClazz.newInstance();
                Method m = hotClazz.getMethod("hot");
                m.invoke(hot, null); //打印出相关信息
                // 每隔10秒重新加载一次
                Thread.sleep(10000);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /**
     * 加载class
     */
    public void initLoad() throws Exception {
        hotSwapCL = HotSwapURLClassLoader.getClassLoader();
        // 如果Hot类被修改了,那么会重新加载,hotClass也会返回新的
        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不是同一个。 ![在此输入图片描述][1] 总结:上述类热加载,需要自定义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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
package testjvm.testclassloader;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
public class SimpleURLClassLoader extends URLClassLoader {
    //工程class类所在的路径
    public static String projectClassPath = "E:/IDE/work_place/ZJob-Note/bin/";
    //所有的测试的类都在同一个包下
    public static String packagePath = "testjvm/testclassloader/";
    public SimpleURLClassLoader() {
        //设置ClassLoader加载的路径
        super(getMyURLs());
    }
    private static  URL[] getMyURLs(){
        URL url = null;
        try {
            url = new File(projectClassPath).toURI().toURL();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
        return new URL[] { url };
    }
    public Class load(String name) throws Exception{
        return loadClass(name);
    }
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name,false);
    }
    /**
     * 重写loadClass,不采用双亲委托机制("java."开头的类还是会由系统默认ClassLoader加载)
     */
    @Override
    public Class<?> loadClass(String name,boolean resolve) throws ClassNotFoundException {
        Class clazz = null;
        //查看HotSwapURLClassLoader实例缓存下,是否已经加载过class
        clazz = findLoadedClass(name);
        if (clazz != null ) {
            if (resolve) {
                resolveClass(clazz);
            }
            return (clazz);
        }
        //如果类的包名为"java."开始,则有系统默认加载器AppClassLoader加载
        if(name.startsWith("java.")) {
            try {
                //得到系统默认的加载cl,即AppClassLoader
                ClassLoader system = ClassLoader.getSystemClassLoader();
                clazz = system.loadClass(name);
                if (clazz != null) {
                    if (resolve)
                        resolveClass(clazz);
                    return (clazz);
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }
        }     
        return customLoad(name,this);
    }
    /**
     * 自定义加载
     * @param name
     * @param cl
     * @return
     * @throws ClassNotFoundException
     */
    public Class customLoad(String name,ClassLoader cl) throws ClassNotFoundException {
        return customLoad(name, false,cl);
    }
    /**
     * 自定义加载
     * @param name
     * @param resolve
     * @return
     * @throws ClassNotFoundException
     */
    public Class customLoad(String name, boolean resolve,ClassLoader cl) throws ClassNotFoundException {
        //findClass()调用的是URLClassLoader里面重载了ClassLoader的findClass()方法
        Class clazz = ((SimpleURLClassLoader)cl).findClass(name);
        if (resolve)
            ((SimpleURLClassLoader)cl).resolveClass(clazz);
        return clazz;
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        return super.findClass(name);
    }
}

代码:A

1
2
3
public class A {
    //  public static final Level CUSTOMLEVEL = new Level("test", 550) {}; // 内部类
}

代码:TestClassUnload,测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package testjvm.testclassloader;
public class TestClassUnLoad {
    public static void main(String[] args) throws Exception {
        SimpleURLClassLoader loader = new SimpleURLClassLoader();
        // 用自定义的加载器加载A
        Class clazzA = loader.load("testjvm.testclassloader.A");
        Object a = clazzA.newInstance();
        // 清除相关引用
        a = null;
        clazzA = null;
        loader = null;
        // 执行一次gc垃圾回收
        System.gc();
        System.out.println("GC over");
    }
}

运行的时候配置VM参数: -verbose:class;用于查看class的加载与卸载情况。 输出结果 ….. [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] …… (1) 启动类加载器加载的类型在整个运行期间是不可能被卸载的(jvm和jls规范). (2) 被系统类加载器和标准扩展类加载器加载的类型在运行期间不太可能被卸载,因为系统类加载器实例或者标准扩展类的实例基本上在整个运行期间总能直接或者间接的访问的到,其达到unreachable的可能性极小.(当然,在虚拟机快退出的时候可以,因为不管ClassLoader实例或者Class(java.lang.Class)实例也都是在堆中存在,同样遵循垃圾收集的规则). (3) 被开发者自定义的类加载器实例加载的类型只有在很简单的上下文环境中才能被卸载,而且一般还要借助于强制调用虚拟机的垃圾收集功能才可以做到.可以预想,稍微复杂点的应用场景中(尤其很多时候,用户在开发自定义类加载器实例的时候采用缓存的策略以提高系统性能),被加载的类型在运行期间也是几乎不太可能被卸载的(至少卸载的时间是不确定的).

综合以上三点, 一个已经加载的类型被卸载的几率很小至少被卸载的时间是不确定的.同时,我们可以看的出来,开发者在开发代码时候,不应该对虚拟机的类型卸载做任何假设的前提下来实现系统中的特定功能.

GIT@OSC工程路径:http://git.oschina.net/taomk/king-training/tree/master/class-loader

Class热替换与卸载相关推荐

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

    所以一个class被一个ClassLoader实例加载过的话,就不能再被这个ClassLoader实例再次加载(这里的加载指的是,调用了defileClass(...)放方法,重新加载字节码.解析.验 ...

  2. 透过现象看本质:Java类动态加载和热替换

    摘要:本文主要介绍类加载器.自定义类加载器及类的加载和卸载等内容,并举例介绍了Java类的热替换. 最近,遇到了两个和Java类的加载和卸载相关的问题: 1) 是一道关于Java的判断题:一个类被首次 ...

  3. 自定义classloader实现JAVA热替换

    这几天学习了下JVM的原理,在看一个视频教程,上面上一个这样的题目: 1. 实现热替换.    运行一个程序HelloMain,他会循环调用另外一个类Worker.doit()方法.此时,对Worke ...

  4. react 动态修改路由_升级到 React Router 4 并实现动态加载和模块热替换

    这篇文章是升级到Webpack2坑--模块热替换失效页面不自动刷新?的后续.那篇文章主要说明了,升级到 Webpack 2 之后,通过升级webpack-dev-server和react-hot-lo ...

  5. 【webpack】-- 模块热替换

    全称是Hot Module ReplaceMent(HMR),理解成热模块替换或者模块热替换都可以吧,和.net中的热插拔一个意思,就是在运行中对程序的模块进行更新.这个功能主要是用于开发过程中,对生 ...

  6. java 热替换 匿名类_Java 类的热替换

    Java 类的热替换 -- 概念.设计与实现 构建基于 Java 的在线升级系统 孙 鸣 和 邓 辉 2010 年 1 月 14 日发布 Java ClassLoader 技术剖析 在本文中,我们将不 ...

  7. [Unity热更新]04.卸载方式

    [Unity热更新]04.卸载方式 参考链接: https://www.jianshu.com/p/b37ee8cea04c 1.AssetBundle.Unload(false):释放AssetBu ...

  8. vue+webpack热替换

    项目地址:http://pan.baidu.com/s/1i5KCXBf 今天上午和同事完成了在mac上面调试了我的框架,最后发现问题出在window系统和mac系统在表示路径的时候出现问题,在解决这 ...

  9. webpack 热替换和热重载

    热替换是利用h5 的新事件 eventSource 实现的,服务器向前端单向推送消息的事件,服务推送给前端,修改的文件 前端请求到该修改的文件插入到页面中,然后执行该js,页面局部渲染出来,原先的执行 ...

最新文章

  1. [转]C#网络编程(异步传输字符串) - Part.3
  2. pandas datafrae merge操作
  3. mysql 5.5 5.6 主从_mysql5.6+主从集的版本号(mysql5.5主机和从机载带后,5.5在设置有一定的差距)...
  4. Spring Cloud 爆高危漏洞!!!
  5. 《面向对象程序设计课程学习进度条》
  6. 《你必须知道的.NET》第1章学习笔记
  7. antlr4 代码 语法树_使用ANTLR4,用于代码镜像和基于Web的DSL的Primefaces扩展
  8. citespace安装如何配置JAVA_citespace超详细安装教程
  9. k近邻法的实现(kd树)-相关问题梳理
  10. 如何开始一个深度学习项目
  11. 【JZOJ5911】Travel
  12. clarity, texture and sharpening
  13. cvSobel() 梯度和Sobel导数
  14. 转型太慢药丸?西数欲举债180亿美元竞购闪迪!
  15. 打开 CMD 的方式+常用的Dos命令
  16. 桌面窗口管理器(dwn.exe)内存占用大怎么解决
  17. 思考与总结【3】:专注于一件事
  18. 程序员杂志在线阅读 豆瓣的架构—专访豆瓣网站的技术总监洪强宁
  19. 最全的android各国语言对照表-转载
  20. PT静态时序分析 第一课 第二课

热门文章

  1. HDU 6608 Fansblog——————大素数检测
  2. jit和jitx区别_JIT,JIS间的区别是什么?
  3. CTF-WEB总结(四-题目来源i春秋)
  4. linux与windows内核哪个难学,国产操作系统为何都基于Linux内核?又和Windows像?
  5. 【转】基金入门:如何买基金?
  6. 完整 Python中切片说明 arr [start: end: step] (arr [-1]、arr[:-1]、arr [::-1] 等的区别)
  7. Spring框架中的单例Bean是线程安全的么?
  8. 2022 Q2 GO开发者问卷调查结果
  9. Word字数统计怎么用?2003/2007/2010统计字数全攻略!
  10. c#获取word文件页数、字数