开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情
一、开篇
本文主要讲解ClassLoader是如何加载和管理类,为方便大家顺畅地理解本文,先介绍本文的思路:
(1) ClassLoader是什么?用来做什么的?常见的BootstrapClassloader、ExtClassLoader、AppClassLoader、SystemClassLoader、他们各是什么?有什么区别?
(2) ClassLoader双亲委派模型是什么?为何有双亲委派模型?
(3) 从源码分析双亲委派模型。
本文主要讲解 JDK8 的ClassLoader机制,其他版本可能有所差异。
二、ClassLoader初探
ClassLoader是什么?
ClassLoader即类加载器,ClassLoader根据一个类的全限定名(例如: java.util.ArrayList)来获取描述该类的二进制字节流,然后将字节流加载到 JVM,解析类信息,生成java.lang.Class对象。
如何确定两个类是否相等?
如何确定两个类是否"相等"?一个ClassLoader实例加一个Class文件唯一确认一个类。也就是,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
这里所指的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance() 方法的返回结果,也包括了使用instanceof关键字做对象所属关系判定等各种情况。
通过下面的例子,我们先体验一下ClassLoader如何加载类以及比较加载自相同class文件的两个类。
首先创建类ClassA
package com.skyme.jvm.classloader;
public class ClassA {
}
再通过ClassLoaderTest来做试验:
package com.skyme.jvm.classloader;
import java.io.IOException;
import java.io.InputStream;
public class ClassLoaderTest {
/**
* 自定义一个ClassLoader
*/
public static class MyClassLoader extends ClassLoader {
/**
* 自定义获取Class文件的方式
*/
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 注意:此处自定义的loadClass(name),没有调用父类ClassLoader的loadClass(name)方法
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (
IOException e) {
throw new ClassNotFoundException(name);
}
}
}
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
// 步骤1. 创建MyClassLoader对象
MyClassLoader myClassLoader = new MyClassLoader();
// 步骤2. 使用MyClassLoader加载"com.skyme.jvm.classloader.ClassA"
Class<?> classA = myClassLoader.loadClass("com.skyme.jvm.classloader.ClassA");
// 步骤3. 使用classA创建实例对象
Object classAObj = classA.newInstance();
// 步骤4. 打印classAObj对应的Class
System.out.println("步骤4 classAObj对应的Class: " + classAObj.getClass());
// 步骤5. 打印Class的ClassLoader
System.out.println("步骤5.1 MyClassLoader加载的com.skyme.jvm.classloader.ClassA对应的ClassLoader: " + classAObj.getClass().getClassLoader());
System.out.println("步骤5.2 xxx.class方式加载的com.skyme.jvm.classloader.ClassA 对应的ClassLoader: " +
com.skyme.jvm.classloader.ClassA.class.getClassLoader());
// 步骤6. 对比class
System.out.println("步骤6.1 MyClassLoader加载的com.skyme.jvm.classloader.ClassA与xxx.class方式加载的com.skyme.jvm.classloader.ClassA是否相等: " +
(classA.equals(com.skyme.jvm.classloader.ClassA.class)));
System.out.println("步骤6.2 classAObj是否是xxx.class方式加载的com.skyme.jvm.classloader.ClassA的实例:" +
(classAObj instanceof com.skyme.jvm.classloader.ClassA));
}
}
上面例子运行结果:
步骤4 classAObj对应的Class: class com.skyme.jvm.classloader.ClassA
步骤5.1 MyClassLoader加载的com.skyme.jvm.classloader.ClassA对应的ClassLoader: com.skyme.jvm.classloader.ClassLoaderTest$MyClassLoader@330bedb4
步骤5.2 xxx.class方式加载的com.skyme.jvm.classloader.ClassA 对应的ClassLoader: sun.misc.Launcher$AppClassLoader@14ae5a5
步骤6.1 MyClassLoader加载的com.skyme.jvm.classloader.ClassA与xxx.class方式加载的com.skyme.jvm.classloader.ClassA是否相等: false
步骤6.2 classAObj是否是xxx.class方式加载的com.skyme.jvm.classloader.ClassA的实例:false
我们自定义了一个MyClassLoader,通过覆写loadClass()方法,实现了根据类名加载com.skyme.jvm.classloader.ClassA
。
通过运行结果里步骤5的结果可看出,com.skyme.jvm.classloader.ClassA.class方式获取的Class对象,该Class对象所属ClassLoader是AppClassLoader,而myClassLoader.loadClass("com.skyme.jvm.classloader.ClassA")方式获取Class对象所属ClassLoader是MyClassLoader。
通过步骤6的结果发现,都是com.skyme.jvm.classloader.ClassA类对应的Class对象,但是通过Class的equals()方法进行对比,他们结果并不相等,且通过instanceof关键字判断发现:MyClassLoader加载的Class对象创建的实例对象classAObj,并不属于com.skyme.jvm.classloader.ClassA(因为这个Class对象是AppClassLoader加载的)。
三、ClassLoader分类
下面通过思维导图整理了ClassLoader的分类。
站在Java开发人员的角度,了解一下BootstrapClassLoader、ExtClassLoader、AppClassLoader,还有自定义ClassLoader。
1 BootstrapClassLoader
主要用以加载核心类库,例如: java.util.List、java.lang.String。BootstrapClass是用C++语言实现,是虚拟机自身的一部分,且无法被Java程序直接引用。
加载的文件
1.BootstrapClassLoader会加载<JAVA_HOME>\lib目录下的类库
2.-Xbootclasspath参数所指定路径的类库
2 ExtClassLoader
BootstrapClassLoader加载核心类库,ExtClassLoader加载的是一些扩展类库,例如:DNS相关的dnsns.jar,对应源码在sun.misc.Launcher.ExtClassLoader
。在JDK9之后,ExtClassLoader被PlatformClassLoader取代。
加载的文件
1.BootstrapClassLoader会加载<JAVA_HOME>\lib\ext目录下的类库
2.java.ext.dirs系统变量所指定路径的类库
ExtClassLoader常见错误用法
1.-Djava.ext.dirs中没有加上<JAVA_HOME>\lib\ext目录
-Djava.ext.dirs会覆盖Java本身的ext设置,java.ext.dirs指定的目录由ExtClassLoader加载器加载,如果您的程序没有指定该系统属性(-Djava.ext.dirs=d:\libs)那么该加载器默认加载<JAVA_HOME>\lib\ext目录下的所有文件。但如果你手动指定系统属性且忘了把<JAVA_HOME>\lib\ext路径给加上,那么ExtClassLoader不会去加载<JAVA_HOME>\lib\ext下面的jar文件,这意味着你将失去一些功能,例如java自带的加解密算法实现,所以如果要设置java.ext.dirs,一般情况,java.ext.dirs也得加上<JAVA_HOME>\lib\ext,示例:-Djava.ext.dirs="lib\ext;%JAVA_HOME%\jre\lib\ext"。
2.-java.ext.dirs指定应用自身类库
实际工作中,看到许多用项目中-java.ext.dirs指定了应用自身类库(应用自身代码、依赖的jar包等),导致用户的包被ExtClassLoader加载,但是很少场景适合用-Djava.ext.dirs去指定类库,建议应用自身类库用-classpath指定,然后由AppClassLoader加载。之前一些项目-java.ext.dirs指定了应用自身类库,然后项目通过Javaagent方式接入skywalking-agent时,就出现找不到类的情况,导致项目无法启动,同事硕总的这篇文章对这个案例做过分析插件的类隔离设计-奇葩问题的解决。
3 AppClassLoader
AppClassLoadr是我们接触最多的ClassLoader,我们应用自身类库(应用自身代码、依赖的jar包等)一般都是通过AppClassLoader加载,当然也有特别的情况,如: SpringBoot应用打包成fatjar方式被启动时,应用中的类库是通过SpringBoot的自定义ClassLoader(即LaunchedURLClassLoader)加载的。
对应源码在sun.misc.Launcher$AppClassLoader。AppClassLoader也称为系统类加载器,因为java.lang.ClassLoader#getSystemClassLoader默认返回的是AppClassLoader。
加载的文件
1.-cp或-classpath指定的路径
2.jar包中MANIFEST.MF文件的Class-Path参数指定的路径
3.CLASSPATH系统环境变量指定的路径
4 自定义ClassLoader
用户也可以自定义ClassLoader,只要继承抽象类 java.lang.ClassLoader,覆写loadClass()方法或者findClass()方法。
自定义的ClassLoader给我们提供了强大的扩展性,我们可以实现:
1.通过各种源获取Class文件
除了获取磁盘上的Class文件,我们还可以在loadClass()或者findClass()方法中获取其他来源的Class文件,如:通过网络获取Class文件。
2.源码加密保护
我们不想自己的Class文件被其他人获取后只能反编译获取源码,则可以在源码编译打包时,先用特定算法对Class文件进行加密,在JVM加载Class文件时再用自定义的ClassLoader进行解密。
3.类隔离
不同的自定义ClassLoader装载的类可相互直接隔离,例如,外置的Tomcat容器就是通过不同自定义ClassLoader的实例去加载不同web应用的类,最终实现了不同web应用间类相互隔离,不会有冲突。Arthas和skywalking的java agent通过自定义的ClassLoader实现了agent的类与宿主应用的类相互隔离,保障agent的类不会污染宿主应用的类。
4.类热加载
通过自定义的ClassLoader,可以在应用运行期间,动态加载有变化的类。热加载工具spring-boot-devtools就是通过自定义的Restart classloader来加载项目主类和有变化的类,实现了类的热加载。
此外,自定义的ClassLoader还有其他妙用,大家可以自己继续补充。
四、双亲委派模型
1 简介
双亲委派机制,对应的英文是parents delegation model
,这里的parent翻译为"双亲"容易有歧义,其实ClassLoader的parent只会有1个,所以翻译为"父委派机制"更贴切一些。
双亲委派模型,简单来讲,就是ClassLoader要加载类时,会先委派给父ClassLoader去加载,父ClassLoader又会委托他自己的父ClassLoader去加载,如果上面的父ClassLoader加载不到,则再交给下一层的父ClassLoader加载,所有父ClassLoader加载不到类,才会交由当前ClassLoader加载。
2 为何有双亲委派模型?
1.防止Java核心类库被开发者的类库篡改。
2.类复用,减少重复加载。例如,java.lang.Object类其实只要由BootstrapClassLoader加载一次就可以,其他ClassLoader不需要再重复加载。
3 双亲委派的类加载过程
下图展示了双亲委派模型下类加载的过程。
上图中AppClassLoader、ExtClassLoader、BootstrapClassLoader的委派机制,大家都比较熟悉。
此外,有两个关注点需要注意:
1.如果ClassLoader类加载成功,则该ClassLoader会缓存该类,下次该ClassLoader就不用再执行类加载动作,直接读缓存返回即可。
2.对于自定义类加载器并非都是会委派给AppClassLoader去加载,即UserClassLoader1如果按上图的委派给AppClassLoader执行加载是有前提的:
(1) UserClassLoader1的parent ClassLoader设置为AppClassLoader
(2) 其次UserClassLoader1没有改动ClassLoader中loadClass()方法里双亲委派的逻辑。
我们下面来根据ClassLoader源码来说明这两个前提。
4 从ClassLoader源码分析双亲委派模型
双亲委派机制的核心逻辑在java.lang.ClassLoader#loadClass(java.lang.String, boolean)
,
/** 父ClassLoader */
private final ClassLoader parent;
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 步骤1. 首先,校验类是否已经加载过了,如果加载过,则直接读缓存返回。
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
// 步骤2 委派parent ClassLoader去加载
try {
// 2.1 如果parent ClassLoader不为空,则委派给parent ClassLoader去执行loadClass()方法加载类
if (parent != null) {
c = parent.loadClass(name, false);
} else { // 2.2 如果parent ClassLoader为空,例如ExtClassLoader的父ClassLoader就是为空,则此时parent ClassLoader就取BootstrapClassLoader。将委派给BootstrapClassLoader去加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) { // 如果抛出ClassNotFoundException,则说明类没有找到
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 步骤3. 如果通过上面的父类委派方式,还是没有找到。则调用findClass()方法去加载类
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
..........
}
}
..........
return c;
}
}
// findClass()方法用以定义当前ClassLoader获取类的方式
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
从上面的源码,几处关键信息,
1.loadClass()中定义了双亲委派模型。
loadClass()定义了双亲委派模型,如果我们不想破坏双亲委派模型,则不建议覆写loadClass()方法,而是覆写findClass()方法自定义获取类的方式即可。
来个思考题,如果自定义的ClassLoader(类名为UserClassLoader2)覆写了loadClass()方法,并且不调用父类java.lang.ClassLoader#loadClass(java.lang.String, boolean)
,会发生什么?
答案是:
自定义的ClassLoader将不遵循双亲委派模型,因为不调用父类java.lang.ClassLoader#loadClass(java.lang.String, boolean)
就没有委派父类加载类的过程了。此时自定义的ClassLoader会自己直接去加载对应类。
对应的加载流程为:
2.ClassLoader的双亲委派机制,其实就是委派给parent ClassLoader去加载。
如果遵循双亲委派机制,那么当parent ClassLoader不为空,就委派给parent ClassLoader去执行loadClass()方法加载类;当parent ClassLoader为空,例如ExtClassLoader的父ClassLoader就是为空,则此时parent ClassLoader就取BootstrapClassLoader,将委派给BootstrapClassLoader去加载类。
来个思考题,如果自定义的ClassLoader(类名为UserClassLoader3)未覆写loadClass()方法,而是parent ClassLoader设置为ExtClassLoader,会发生什么?
答案是:
此时UserClassLoader3会遵循双亲委派机制,但是,UserClassLoader3加载类时,不会委派给AppClassLoader加载,而是先委派给ExtClassLoader。
对应的加载流程为:
五、小结
本文讲解了ClassLoader的分类,ClassLoader加载类的过程,以及通过源码分析了双亲委派模型的原理和注意事项。
六、参考
《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明》