JAVA ClassLoader

本文从虚拟机底层机制说起,然后分析ClassLoader代码结构与特性,最后说明ClassLoader在9.0插件引擎中的应用。

Class对象的生命周期

加载
验证
准备
解析
初始化
这里简化一下,只关注加载和初始化。加载是指将二进制字节码转化为Class对象的过程,初始化是指执行Class中static方法块,生成static变量等。

触发加载过程的操作有:

显式调用classLoader.loadClass(className)
在代码执行过程(比如当前类的初始化)中,使用了其他的类,虚拟机会自动加载(不一定会初始化)
ClassLoader在显示调用中的作用不言而喻。在代码执行过程中,如果用到了其他类,虚拟机会首先获取当前类的ClassLoader,再通过这个ClassLoader去查找引用的类,如果没有找到,则会触发加载操作,如果也没加载到,就会抛ClassNotFoundException。

触发初始化的操作有:

显示初始化:Class.forName(className)
创建类的实例
调用类的静态方法
使用类的非常量静态字段
初始化某个类的字类
通常一个类的加载不会触发其他类的加载,一个类的初始化会触发其他类的加载和初始化。

不同ClassLoader对类的隔离

通过上面Class的初始化过程可以看到,一个类能够依赖什么类,完全是取决于他的ClassLoader能找到哪些类。如果两个ClassLoader没有父子关系,那么他们加载的类是永远也不能相互引用到的。同样,如果两个ClassLoader,A和B,没有父子关系。A首先加载了classC,B中的类在使用过程中,也需要用到classC,则会触发B的加载操作(虽然内存中已经有了classC的Class<?>对象),这时内存中就会存在两个classC的Class<?>对象,这两个并不相等,没有任何关系。这样就保证了A中的类用到的classC都是A自己加载的,B中的类用到的classC都是B加载的,彼此共存又不互相打扰。

例如:

其中,PluginClassLoaderA是插件更新前的classLoader,PluginClassLoaderB是插件更新后的ClassLoader。在插件停止、删除并更新后,老版本的Class对象会由虚拟机在合适的实际处理掉,也就是说,在一定的时间之内,新版本的Class对象和老版本的Class对象是在内存里面共存的。

ClassLoader的查找机制保证了新老插件的Class不会相互影响。比如在升级后,右边的Class4触发了com.fr.plugin.A的加载,虚拟机首先拿到Class4的ClassLoader,即PluginClassLoaderB,然后用PluginClassLoaderB去加载com.fr.plugin.A,在查找加载过程中,PluginClassLoaderB首先委托parent,即ParentClassLoader去查找,没有找到则自己从右下角的四个类里面去找,找到则返回,不会访问左下角的四个类,保证了新老插件的隔离。

ClassLoader的结构

除去资源加载的方法外,ClassLoader只有一个public的loadClass方法。这个方法返回一个Class对象或抛出ClassNotFoundException。

//loadClass方法(这里精简了一下)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public Class<?> loadClass(String name) throws ClassNotFoundException {
//首先检查已经是否已经加载过了
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
//双亲委派
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException ignore) {
}
//自己没加载过,同时parent也找不到的话
if (c == null) {
//尝试查找对应字节码,加载新的Class
c = findClass(name);
}
}
return c;
}

这个方法是双亲委派机制的实现,也是sun说的最好不要重写loadClass方法,要重写就写findClass的原因,重写loadClass可能会打破双亲委派机制。

有了双亲委派机制,就可以通过定义不同ClassLoader之间的关系,来做到不同插件之间共享报表的类,又隔离不同插件之间的类。

默认的findClass方法是这个样子的:

1
2
3
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

相当于是只定义了接口,没有实现。接口的含义就是:通过一个类的全限定名,返回一个Class对象。同时,ClassLoader提供了几个返回Class对象的方法可供调用:

1
2
3
4
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain) throws ClassFormatError
protected final Class<?> defineClass(String name, java.nio.ByteBuffer, ProtectionDomain protectionDomain)

这三个方法就是我们用来定义Class的。有了这三个方法, 我们重写findClass的任务就是获取类的字节码而已。这里就是我们可以自由发挥的地方了,可以从文件中读,可以从网络中读,还可以从加密的文件中读。读来字节码之后,还能对字节码进行处理(见3、Javassist的使用)后再调用defineClass。