文章目录
- 1 类加载器
- 1.1 类加载器介绍
- 1.2 类加载器的加载规则
- 1.2 类加载器类型总结
- 2 双亲委派模型
- 2.1 双亲委派模型介绍
- 2.2 双亲委派模型的执行流程
- 2.3 双亲委派模型的好处
回顾一下类加载过程:加载->连接->初始化。
其中连接又分为:验证->准备->解析
加载是类加载过程的第一步,主要完成下面 3 件事情:
- 通过全类名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口
1 类加载器
1.1 类加载器介绍
根据官方 API 文档的介绍:
类加载器是一个负责加载类的对象。
ClassLoader
是一个抽象类。给定类的二进制名称,类加载器应尝试定位或生成构成类定义的数据。典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”。每个 Java 类都有一个引用指向加载它的ClassLoader
。不过,数组类不是通过ClassLoader
创建的,而是 JVM 在需要的时候自动创建的,数组类通过getClassLoader()
方法获取ClassLoader
的时候和该数组的元素类型的ClassLoader
是一致的。
从上面的介绍可以看出:
- 类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步
- 每个 Java 类都有一个引用指向加载它的 ClassLoader。
- 数组类不是通过 ClassLoader 创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的。
class Class<T> {
...
private final ClassLoader classLoader;
@CallerSensitive
public ClassLoader getClassLoader() {
//...
}
...
}
简单来说,类加载器的主要作用就是加载 Java 类的字节码( .class 文件)到 JVM 中(在内存中生成一个代表该类的 Class 对象)。
1.2 类加载器的加载规则
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);
}
...
}
1.2 类加载器类型总结
JVM 中内置了三个重要的 ClassLoader
:
- BootstrapClassLoader(启动类加载器) :最顶层的加载类,由 C++实现,通常表示为 null,并且没有父级,主要用来加载 JDK 内部的核心类库( %JAVA_HOME%/lib目录下的 rt.jar 、resources.jar 、charsets.jar等 jar 包和类))以及被 -Xbootclasspath参数指定的路径下的所有类。
- ExtensionClassLoader(扩展类加载器) :主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类。
- AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。
rt.jar : rt 代表“RunTime”,rt.jar是Java基础类库,包含Java doc里面看到的所有的类的类文件。也就是说,我们常用内置库 java.xxx.* 都在里面,比如java.util.、java.io.、java.nio.、java.lang.、java.sql.、java.math.。
除了 BootstrapClassLoader
是 JVM 自身的一部分之外,其他所有的类加载器都是在 JVM 外部实现的,并且全都继承自 ClassLoader
抽象类。这样做的好处是用户可以自定义类加载器,以便让应用程序自己决定如何去获取所需的类。
每个 ClassLoader
可以通过getParent()
获取其父 ClassLoader
,如果获取到 ClassLoader
为null
的话,那么该类是通过 BootstrapClassLoader
加载的。( 这是因为BootstrapClassLoader
由 C++ 实现,由于这个 C++ 实现的类加载器在 Java 中是没有与之对应的类的,所以拿到的结果是 null
。)
public abstract class ClassLoader {
...
// 父加载器
private final ClassLoader parent;
@CallerSensitive
public final ClassLoader getParent() {
//...
}
...
}
2 双亲委派模型
2.1 双亲委派模型介绍
类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载呢?这就需要提到双亲委派模型了。根据官网介绍:
ClassLoader
类使用委托模型来搜索类和资源。每个ClassLoader
实例都有一个相关的父类加载器。需要查找类或资源时,ClassLoader
实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。 虚拟机中被称为 "bootstrap class loader
"的内置类加载器本身没有父类加载器,但是可以作为ClassLoader
实例的父类加载器。
可以看出:
ClassLoader
类使用委托模型来搜索类和资源。- 双亲委派模型要求除了顶层的
bootstrap classloader
外,其余的类加载器都应有自己的父类加载器。 ClassLoader
实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。
类加载器之间的父子关系一般不是以继承的关系来实现的,而是通常使用组合关系来复用父加载器的代码。
public abstract class ClassLoader {
...
// 组合
private final ClassLoader parent;
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
...
}
组合优于继承,多用组合少用继承。
2.2 双亲委派模型的执行流程
双亲委派模型的实现代码非常简单,逻辑非常清晰,都集中在 java.lang.ClassLoader
的 loadClass()
中,相关代码如下所示。
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;
}
}
执行流程如下:
- 先去检查该类是否已经被加载过,已经加载的类会被直接返回,否则进行步骤2
- 将加载的任务交给父类加载器,如果父类加载器为null,则交给
bootstrap classloader
, 这样的话,所有的请求最终都会传送到顶层的启动类加载器BootstrapClassLoader
中。 - 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的
findClass()
方法来加载类)。
JVM 判定两个 Java 类是否相同的具体规则 :JVM 不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相同。
2.3 双亲委派模型的好处
- 避免重复加载:由于类加载器会在其加载的类中缓存已经加载的类,所以使用双亲委派模型可以避免重复加载同一个类,从而节省内存空间。
- 防止类库被篡改:由于父类加载器可以优先加载类库,因此双亲委派模型可以避免用户篡改Java核心类库等安全问题。
- 保证类的一致性:如果父类加载器已经加载了一个类,子类加载器再次尝试加载该类时,由于双亲委派模型的存在,子类加载器将不会再次加载该类,而是直接使用父类加载器所加载的类,从而保证类的一致性。
- 支持类的版本管理:由于Java类库升级时,只需要升级父类加载器中的类库即可,从而避免类库版本不一致的问题。