# 4.6 热加载与卸载

在类的加载过程中,我们知道会先检查该类是否已经加载,如果已经加载了,则不会从jar包或者路径上查找类, 而是使用缓存中的类。JVM表示一个类是否是相同的类有两个条件:第一个是类的全限定名称是否相同,第二个是类的加载器实例是否是同一个。 因此要实现类的热加载,可以使用不同的类加载器来加载同一个类文件。

使用不同的类加载器实例加载同一个类文件,随着加载次数增加,类的个数也会不断增加,如果不及时清理元空间/永久代,会有内存溢出的风险。 然而类卸载的条件非常苛刻,一般要同时具备下面的三个条件才可以卸载,并且需要JVM执行fullgc后才能完全清除干净。类卸载的三个条件(来源于JVM虚拟机规范)。

  • 该类所有的实例都已经被GC;

  • 加载该类的ClassLoader实例已经被GC;

  • 该类的java.lang.Class对象没有在任何地方被引用;

full GC的时机我们是不可控的,那么同样的我们对于Class的卸载也是不可控的。 从上面的三个条件可以看出JVM自带的类加载器不会被回收,因此JVM的类不会被卸载。只有自定义类加载器才有卸载的可能。 下面给出一个具体的需求,并使用热加载来完成。应用在运行时加载一个class脚本,class脚本可以做到热更新。 有这样一个脚本接口,具有获取版本号和执行运算的功能。

public interface Script {
    // 执行运算
    String run(String key);
}
1
2
3
4

脚本的实现类,负责具体的计算功能。

public class ScriptImpl implements Script {

    public ScriptImpl() {
    }

    public String run(String key) {
        return key;
    }
}
1
2
3
4
5
6
7
8
9

JVM运行过程中替换脚本的实现,既可以实现脚本的更新功能。

public class Main {
    public static void main(String[] args) throws Exception {
        ClassLoader appClassloader = Main.class.getClassLoader();

        ScriptClassLoader scriptClassLoader1 = new ScriptClassLoader("resources", appClassloader);
        Class<?> scriptImpl1 = scriptClassLoader1.loadClass("ScriptImpl");
        System.out.println(scriptImpl1.hashCode());

        ScriptClassLoader scriptClassLoader2 = new ScriptClassLoader("resources", appClassloader);
        Class<?> scriptImpl2 = scriptClassLoader2.loadClass("ScriptImpl");
        
        // class对象不相同
        assert scriptImpl1 != scriptImpl2;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

使用不同的类加载器加载同一个类,得到的class对象不一样,运行时更新ScriptImpl类的实现即可。ScriptClassLoader的实现如下。

public class ScriptClassLoader extends ClassLoader {
    private String classDir;

    public ScriptClassLoader(String classDir,ClassLoader classLoader) {
        super(classLoader);
        this.classDir = classDir;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] classDate = getDate(name);
            if (classDate == null) {
                return null;
            }
            return defineClass(name, classDate, 0, classDate.length);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private byte[] getDate(String className) throws IOException {
        InputStream in = null;
        ByteArrayOutputStream out = null;
        String path = classDir + File.separatorChar +
                className.replace('.', File.separatorChar) + ".class";
        try {
            in = new FileInputStream(path);
            out = new ByteArrayOutputStream();
            byte[] buffer = new byte[2048];
            int len = 0;
            while ((len = in.read(buffer)) != -1) {
                out.write(buffer, 0, len);
            }
            return out.toByteArray();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            in.close();
            out.close();
        }
        return null;
    }
}
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