概述
- 类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。
- 每个 Java 类都有一个引用指向加载它的
ClassLoader
。 - 数组类不是通过
ClassLoader
创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的。
简单来说,类加载器的主要作用就是加载 Java 类的字节码( .class
文件)到 JVM 中(在内存中生成一个代表该类的 Class
对象)。
类加载规则
JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加友好。
对于已经加载的类会被放在 ClassLoader
中。
- 在类加载的时候,系统会首先判断当前类是否被加载过。
- 已经被加载的类会直接返回,否则才会尝试加载。
也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。
public abstract class ClassLoader {
...
private final ClassLoader parent;
// 由这个类加载器加载的类。
private final Vector<Class<?>> classes = new Vector<>();
// 由VM调用,用此类加载器记录每个已加载类。
void addClass(Class<?> c) {
classes.addElement(c);
}
...
}
类加载器种类
JVM 中内置了三个重要的 ClassLoader
:
BootstrapClassLoader
(启动类加载器) :最顶层的加载类,没有父级。Bootstrap ClassLoader是C++写的,它本身是虚拟机的一部分,并不是一个JAVA类,也就是无法在java代码中获取它的引用(其他所有的类加载器都是在 JVM 外部实现的,并且全都继承自ClassLoader
抽象类)。该加载器主要用来加载 JDK 内部的核心类库,包括:%JAVA_HOME%/lib
目录下的rt.jar
(Java 基础类库)、resources.jar
、charsets.jar
等 jar 包和类)。- 被
-Xbootclasspath
参数指定的路径下的所有类。
ExtensionClassLoader
(扩展类加载器) :- 主要负责加载
%JRE_HOME%/lib/ext
目录下的 jar 包和类 - 以及被
java.ext.dirs
系统变量所指定的路径下的所有类。
- 主要负责加载
AppClassLoader
(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。
除了这三种类加载器之外,用户还可以加入自定义的类加载器来进行拓展,以满足自己的特殊需求。就比如说,我们可以对 Java 类的字节码( .class
文件)进行加密,加载时再利用自定义的类加载器对其解密。
案例:获取ClassLoader
public class PrintClassLoaderTree {
public static void main(String[] args) {
ClassLoader classLoader = PrintClassLoaderTree.class.getClassLoader();
StringBuilder split = new StringBuilder("|--");
boolean needContinue = true;
while (needContinue){
System.out.println(split.toString() + classLoader);
if(classLoader == null){
needContinue = false;
}else{
classLoader = classLoader.getParent();
split.insert(0, "\t");
}
}
}
}
输出结果
|--sun.misc.Launcher$AppClassLoader@18b4aac2
|--sun.misc.Launcher$ExtClassLoader@53bd815b
|--null
结论:
- 我们编写的 Java 类
PrintClassLoaderTree
的ClassLoader
是AppClassLoader
; AppClassLoader
的父ClassLoader
是ExtClassLoader
;ExtClassLoader
的父ClassLoader
是Bootstrap ClassLoader
,因此输出结果为 null。
类加载器选择规则
双亲委派模型概述
类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载呢?这就需要提到双亲委派模型了。
ClassLoader
类使用委托模型来搜索类和资源。- 双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。
ClassLoader
实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。bootstrap class loader
的内置类加载器本身没有父类加载器,但是可以作为ClassLoader
实例的父类加载器。
下图中的类加载层次图就是双亲委派模型。
(图源:javaguide.cn)
双亲委派模型执行流程
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//首先,检查该类是否已经加载过
Class c = findLoadedClass(name);
if (c == null) {
//如果 c 为 null,则说明该类没有被加载过
long t0 = System.nanoTime();
try {
if (parent != null) {
//当父类的加载器不为空,则通过父类的loadClass来加载该类
c = parent.loadClass(name, false);
} else {
//当父类的加载器为空,则调用启动类加载器来加载该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//非空父类的类加载器无法找到相应的类,则抛出异常
}
if (c == null) {
//当父类加载器无法加载时,则调用findClass方法来加载该类
//用户可通过覆写该方法,来自定义类加载器
long t1 = System.nanoTime();
c = findClass(name);
//用于统计类加载器相关的信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//对类进行link操作
resolveClass(c);
}
return c;
}
}
总结:
- 在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。
- 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器
loadClass()
方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器BootstrapClassLoader
中。 - 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的
findClass()
方法来加载类)。 - 如果子类加载器也无法加载这个类,那么它会抛出一个
ClassNotFoundException
异常。
🌈 拓展一下:
JVM 判定两个 Java 类是否相同的具体规则:JVM 不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即使两个类来源于同一个 Class
文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相同。
双亲委派模型的好处
- 双亲委派模型可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类)
- 也保证了 Java 的核心 API 不被篡改。
如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如:
- 我们编写一个称为
java.lang.String
类的话,那么程序运行的时候,系统就会出现两个不同的String
类。 - 双亲委派模型可以保证加载的是 JRE 里的那个
String
类,而不是你写的String
类。 - 这是因为
AppClassLoader
在加载你的String
类时,会委托给ExtClassLoader
去加载,而ExtClassLoader
又会委托给BootstrapClassLoader
,BootstrapClassLoader
发现自己已经加载过了String
类,会直接返回,不会去加载你写的String
类。