ClassLoader
(类加载器)是负责加载class文件到JVM的抽象类。
扩展 Java 虚拟机动态加载类的方式,需要继承 ClassLoader
,并重写findClass()
或loadClass()
等方法。
findClass()
和loadClass()
关系
findClass()
是一个没有没有实质内容的方法,其方法体直接抛出ClassNotFoundException
异常,访问类型是 protected
,其目的就是为了被重写,重写findClass()
方法也是官方推荐的方式。findClass()
的源码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14/*
* 使用指定的 二进制名称(全路径名)查找类。此方法应该被类加载器的实现重写,
* 该实现按照委托模型(双亲委派)来加载类。在通过父类加载器检查所请求的类后,
* 此方法将被 loadClass 方法调用。默认实现抛出一个 ClassNotFoundException。
*
* @param name 类的 二进制名称(全路径名)
* @return The resulting <tt>Class</tt> object
* @throws ClassNotFoundException If the class could not be found
*
* @since 1.2
*/
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
注释摘录自 JDK1.6 中文版文档。 从注释中可以看出,loadClass()
会调用findClass()
来查找类。loadClass()
源码摘录如下:
1 | protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { |
双亲委派
谈 ClassLoader
必备的一个概念就是 双亲委派。
如上面loadClass()
源码所示,加载一个类,先问 父加载器 有没有,类似于这种形式,人们起了一个高大上的名字叫 双亲委派。
需要注意的是 父子之间并不是继承的关系,而是组合关系。创建 ClassLoader 的时候可以传入 parent ClassLoader。
下面是一个测试:1
2
3
4
5
6
7
8
9
10
11
12// 输出 sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(Thread.currentThread().getContextClassLoader());
// 输出 sun.misc.Launcher$ExtClassLoader@5ca881b5
System.out.println(Thread.currentThread().getContextClassLoader().getParent());
// 输出 null
System.out.println(Thread.currentThread().getContextClassLoader().getParent().getParent());
// 输出 sun.misc.Launcher$ExtClassLoader@5ca881b5
System.out.println(com.sun.nio.zipfs.ZipInfo.class.getClassLoader());
// 输出 null
System.out.println(String.class.getClassLoader());
可以看出,当前线程的 ClassLoader 是 sun.misc.Launcher$AppClassLoader
,其父是sun.misc.Launcher$ExtClassLoader
,其祖是 null。com.sun.nio.zipfs.ZipInfo
的 ClassLoader 直接就是 sun.misc.Launcher$ExtClassLoader
。String
的 ClassLoader 是 null。
以下给出解释
BootStrap ClassLoader
:启动类加载器,负责加载存放在%JAVA_HOME%\lib
目录中的,或者通被-Xbootclasspath
参数所指定的路径中的,并且被java虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库,即使放在指定路径中也不会被加载)类库到虚拟机的内存中,启动类加载器无法被java程序直接引用。
Extension ClassLoader
:扩展类加载器,由sun.misc.Launcher$ExtClassLoader
实现,负责加载%JAVA_HOME%\lib\ext
目录中的,或者被java.ext.dirs
系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
Application ClassLoader
:应用程序类加载器,由sun.misc.Launcher$AppClassLoader
实现,负责加载用户类路径classpath上所指定的类库,是类加载器ClassLoader中的getSystemClassLoader()方法的返回值,开发者可以直接使用应用程序类加载器,如果程序中没有自定义过类加载器,该加载器就是程序中默认的类加载器。
重写 findClass()
1 | import java.io.IOException; |
这里使用上篇 Javassit 生成的 /Users/kail/_test/xyz/kail/blog/CodeClass.class
文件。
1 | MyClassLoader myClassLoader = new MyClassLoader(); |
重写 loadClass()
打破双亲委派
1 | import java.io.IOException; |
这简单的例子打破了双亲委派的模型,没有先从父加载器加载类,而是先从自定的路径下加载,加载之后进行缓存。
清除缓存的时候又 重新 new 了一个 MyClassLoader,因为同一个ClassLoader 无法加载一个类文件两次,会报以下错误java.lang.LinkageError: loader (instance of MyClassLoader): attempted duplicate class definition for name: "xyz/kail/blog/CodeClass"
扩展一下的话,可以用一个线程扫描类路径的下的class文件有没有变化,如果有清掉缓存重新加载,可以实现一个简单的热加载功能。
以上纯属意淫,实际上实现热加载还是很复杂的,要解决类的之间的依赖关系等很多问题,新加载的类没不会保存运行时的各种信息的。
PS
深入了解的话可以通过查看 ClassLoader 的继承结构(IDEA 是 Ctrl+H),查看其它开源项目的ClassLoader 实现,这里不再深入了解。
例如 热加载、代码保护、Tomcat项目隔离、热更新等。