ClassLoader 即类加载器,其作用是在JVM虚拟机中动态的加载class文件。众所周知,我们写的Java应用程序都是由不同的 .class 文件组成的,当运行某段逻辑功能时,就会由一个class文件中的方法调用另一个class文件的方法,若此时调用的class文件找不到就会出现异常。而JVM启动时,并不会将所有的class文件一次全部加载进来,而是会根据需要动态的加载,只有class文件被加载到内存后,才能被其他class文件引用,这个过程中ClassLoader发挥着重要的作用。
一. Class文件
还记得最开始学习Java语言的时候,推荐的方法是先不要过度依赖IDE,而是使用类似记事本的文本编辑器来写的,步骤如下:
- 新建文件,文件格式是
.java
,编写代码逻辑,eg: HelloWorld.java; - 编译
.java
文件,生成.class
文件,命令:javac HelloWorld.java
; - 运行代码 命令:
java HelloWorld
,此时可以运行我们写的代码逻辑;
其实这个才是我们最开始学习的Java文件编译运行方式,只是习惯使用IDE后,IDE帮助我们做了很多工作。正常的.java
格式文件是不能直接运行的,JVM无法识别,而.class
文件是字节码格式文件,JVM可识别,而加载.class
文件使JVM可识别,就是依靠ClassLoader。
二. Java默认的ClassLoader
- 启动类加载器:Bootstrap ClassLoader,主要加载JDK中的核心类库,如目录
%JRE_HOME%\lib
下的rt.jar、resources.jar、charsets.jar和class等; - 扩展类加载器:Extention ClassLoader,主要加载目录
%JRE_HOME%\lib\ext
目录下的jar包和class文件; - 应用类加载器:App ClassLoader,加载当前应用的classpath的所有类;
三. 加载过程
1.加载顺序:
(1)Bootstrap ClassLoader;
(2)Extention ClassLoader;
(3)App ClassLoader;
2.源码分析
且通过查看源码,即JVM入口类sun.misc.Launcher
,其中有BootstrapClassLoader、ExtClassLoader 和 AppClassLoader 的初始化代码,他们实际上是通过查阅环境属性来加载资源文件的,其对应关系为:
- BootstrapClassLoader:
sun.boot.class.path
- ExtClassLoader:
java.ext.dirs
- AppClassLoader:
java.class.path
可以通过获取相应的环境属性来输出路径内容,如
System.out.println(System.getProperty("java.class.path"));
3.每个类加载器都有一个父加载器
- 一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader;
- AppClassLoader的parent是ExtClassLoader;
- ExtClassLoader的parent是null;
- Bootstrap没有父加载器,但是它却可以作为任何一个ClassLoader的父加载器(下面会有分析);
4.父加载器不是父类
- ExtClassloader 和 AppClassLoader 都继承自java.lang.ClassLoader类;
- 用户自定义的ClassLoader需要继承java.lang.ClassLoader类;
- BootStrapClassLoader不继承自java.lang.ClassLoader,因为它不是一个Java类,已经嵌入在JVM内核中,是虚拟机的一部分;
四. 双亲委派
1.双亲委派机制
ClassLoader是使用双亲委派模型来搜索类的。委托是从下向上,查找过程是自上至下:
过程如下:
(1) 收到Class文件请求,首先委派给AppClassLoader,首先看是否有缓存,如果有,则返回;否则将请求委托给父加载器;
(2)父加载器递归执行第1步;
(3)最终达到顶级父加载器BootstrapClassLoader,如果也没有缓存,就去规定路径下sun.misc.boot.path
下查找,找到则返回;没有则让子加载器去查找;
(4)ExtClassLoader在java.ext.dirs
下查找,找到则返回;没有则让子加载器查找;
(5) AppClassLoader在java.class.path
下查找,找到就返回;没有则让子类加载器查找;
(6)如果子类加载器也找不到,则抛出异常。
2.源码分析 loadClass()
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检测是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//父加载器不为空则调用父加载器的loadClass
c = parent.loadClass(name, false);
} else {
//父加载器为空则调用Bootstrap Classloader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//父加载器没有找到,则调用findclass
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//调用resolveClass()
resolveClass(c);
}
return c;
}
}
代码解释了双亲委派机制,步骤如下:
(1) findLoadedClass
方法来检查该类是否已经加载过;
(2)若没有加载过,则调用父类的loadClass方法;
(3)递归执行步骤2,查找该Class对象;
(4)若直到BootstrapClassLoader依然没有加载成功,则通过findClass查找;
(5)findClass在各自的对应路径下查找,若没有则让子加载器查找,直到找到;否则抛出异常;
(6)最后若找到Class对象,且参数resolve为true,调用resolveClass(Class)方法来生成最终的Class对象,且该方法为native方法;
注意1:上面有提到ExtClassLoader的父加载器通过代码执行,结果显示为null,但在实际向上委托时,系统会为其指定为BootstrapClassLoader,代码如下:
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
注意2:要自定义一个ClassLoader,建议覆盖findClass()方法,而不要直接改写loadClass()方法。(原因:JDK已在loadClass方法中实现了ClassLoader搜索类的算法,当在loadClass方法中搜索不到类时,loadClass方法就会调用findClass方法来搜索类,所以只需重写该方法即可。如没有特殊的要求,一般不建议重写loadClass搜索类的算法。)
3.为什么使用双亲委派机制
- 避免重复加载,当父类加载过,就不用子类再加载一遍;
- 安全原因,避免子类自定义的类动态定义Java核心API定义的类;
- JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。
五. 自定义ClassLoader
Java中自带的三种ClassLoader包括 BootStrapClassLoader、ExtClassLoader和AppClassLoader,但他们都是加载指定路径下的jar或者class文件,如果想要突破路径限制,能够加载我们指定路径的文件,就需要自定义ClassLoader。在自定义ClassLoader中,比较重要的一个方法是defineClass()
,能将class二进制内容转换成Class对象,如果不符合要求的会抛出各种异常。
自定义ClassLoader步骤
- 编写一个类继承自ClassLoader抽象类;
- 复写它的findClass()方法;
- 在findClass()方法中调用defineClass();
参考资料
https://blog.csdn.net/briblue/article/details/54973413
https://blog.csdn.net/xyang81/article/details/7292380