# 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
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
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
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
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