回顾一下类加载过程
开始介绍类加载器和双亲委派模型之前,简单回顾一下类加载过程。
-
类加载过程:
加载->连接->初始化
。
-
连接过程又可分为三步:
验证->准备->解析
。
加载是类加载过程的第一步,主要完成下面 3 件事情:
-
通过全类名获取定义此类的二进制字节流
-
将字节流所代表的静态存储结构转换为方法区的运行时数据结构
-
在内存中生成一个代表该类的
Class
对象,作为方法区这些数据的访问入口
类加载器
类加载器介绍
类加载器从 JDK 1.0 就出现了,最初只是为了满足 Java Applet(已经被淘汰) 的需要。后来,慢慢成为 Java 程序中的一个重要组成部分,赋予了 Java 类可以被动态加载到 JVM 中并执行的能力。
根据官方 API 文档的介绍:
A class loader is an object that is responsible for loading classes. The class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to locate or generate data that constitutes a definition for the class. A typical strategy is to transform the name into a file name and then read a "class file" of that name from a file system.
Every Class object contains a reference to the ClassLoader that defined it.
Class objects for array classes are not created by class loaders, but are created automatically as required by the Java runtime. The class loader for an array class, as returned by Class.getClassLoader() is the same as the class loader for its element type; if the element type is a primitive type, then the array class has no class loader.
翻译过来大概的意思是:
类加载器是一个负责加载类的对象。
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
对象)。
字节码可以是 Java 源程序(
.java
文件)经过
javac
编译得来,也可以是通过工具动态生成或者通过网络下载得来。
其实除了加载类之外,类加载器还可以加载 Java 应用所需的资源如文本、图像、配置文件、视频等等文件资源。本文只讨论其核心功能:加载类。
类加载器加载规则
JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加友好。
对于已经加载的类会被放在
ClassLoader
中。在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。
public abstract class ClassLoader {
...
private final ClassLoader parent;
private final Vector<Class<?>> classes = new Vector<>();
void addClass(Class<?> c) {
classes.addElement(c);
...
类加载器总结
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.*
。
-
Java 9 引入了模块系统,并且略微更改了上述的类加载器。扩展类加载器被改名为平台类加载器(platform class loader)。Java SE 中除了少数几个关键模块,比如说
java.base
是由启动类加载器加载之外,其他的模块均由平台类加载器所加载。
除了这三种类加载器之外,用户还可以加入自定义的类加载器来进行拓展,以满足自己的特殊需求。就比如说,我们可以对 Java 类的字节码(
.class
文件)进行加密,加载时再利用自定义的类加载器对其解密。
除了
BootstrapClassLoader
是 JVM 自身的一部分之外,其他所有的类加载器都是在 JVM 外部实现的,并且全都继承自
ClassLoader
抽象类。这样做的好处是用户可以自定义类加载器,以便让应用程序自己决定如何去获取所需的类。
每个
ClassLoader
可以通过
getParent()
获取其父
ClassLoader
,如果获取到
ClassLoader
为
null
的话,那么该类是通过
BootstrapClassLoader
加载的。
public abstract class ClassLoader {
...
private final ClassLoader parent;
@CallerSensitive
public final ClassLoader getParent() {
...
为什么 获取到
ClassLoader
为
null
就是
BootstrapClassLoader
加载的呢?
这是因为
BootstrapClassLoader
由 C++ 实现,由于这个 C++ 实现的类加载器在 Java 中是没有与之对应的类的,所以拿到的结果是 null。
下面我们来看一个获取
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");
输出结果(JDK 8 ):
|--sun.misc.Launcher$AppClassLoader@18b4aac2
|--sun.misc.Launcher$ExtClassLoader@53bd815b
|--null
从输出结果可以看出:
-
我们编写的 Java 类
PrintClassLoaderTree
的
ClassLoader
是
AppClassLoader
;
-
AppClassLoader
的父
ClassLoader
是
ExtClassLoader
;
-
ExtClassLoader
的父
ClassLoader
是
Bootstrap ClassLoader
,因此输出结果为 null。
自定义类加载器
我们前面也说说了,除了
BootstrapClassLoader
其他类加载器均由 Java 实现且全部继承自
java.lang.ClassLoader
。如果我们要自定义自己的类加载器,很明显需要继承
ClassLoader
抽象类。
ClassLoader
类有两个关键的方法:
-
protected Class loadClass(String name, boolean resolve)
:加载指定二进制名称的类,实现了双亲委派机制 。
name
为类的二进制名称,
resolve
如果为 true,在加载时调用
resolveClass(Class<?> c)
方法解析该类。
-
protected Class findClass(String name)
:根据类的二进制名称来查找类,默认实现是空方法。
官方 API 文档中写到:
Subclasses of
ClassLoader
are encouraged to override
findClass(String name)
, rather than this method.
建议
ClassLoader
的子类重写
findClass(String name)
方法而不是
loadClass(String name, boolean resolve)
方法。
如果我们不想打破双亲委派模型,就重写
ClassLoader
类中的
findClass()
方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写
loadClass()
方法。
双亲委派模型
双亲委派模型介绍
类加载器有很多种,当我们想要加载一个类的时候,具体是哪个类加载器加载呢?这就需要提到双亲委派模型了。
根据官网介绍:
The ClassLoader class uses a delegation model to search for classes and resources. Each instance of ClassLoader has an associated parent class loader. When requested to find a class or resource, a ClassLoader instance will delegate the search for the class or resource to its parent class loader before attempting to find the class or resource itself. The virtual machine's built-in class loader, called the "bootstrap class loader", does not itself have a parent but may serve as the parent of a ClassLoader instance.
翻译过来大概的意思是:
ClassLoader
类使用委托模型来搜索类和资源。每个
ClassLoader
实例都有一个相关的父类加载器。需要查找类或资源时,
ClassLoader
实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。 虚拟机中被称为 "bootstrap class loader"的内置类加载器本身没有父类加载器,但是可以作为
ClassLoader
实例的父类加载器。
从上面的介绍可以看出:
-
ClassLoader
类使用委托模型来搜索类和资源。
-
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。
-
ClassLoader
实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。
下图展示的各种类加载器之间的层次关系被称为类加载器的“
双亲委派模型(Parents Delegation Model)
”。
注意 ⚠️:双亲委派模型并不是一种强制性的约束,只是 JDK 官方推荐的一种方式。如果我们因为某些特殊需求想要打破双亲委派模型,也是可以的,后文会介绍具体的方法。
其实这个双亲翻译的容易让别人误解,我们一般理解的双亲都是父母,这里的双亲更多地表达的是“父母这一辈”的人而已,并不是说真的有一个
MotherClassLoader
和一个
FatherClassLoader
。个人觉得翻译成单亲委派模型更好一些,不过,国内既然翻译成了双亲委派模型并流传了,按照这个来也没问题,不要被误解了就好。
另外,类加载器之间的父子关系一般不是以继承的关系来实现的,而是通常使用组合关系来复用父加载器的代码。
public abstract class ClassLoader {
...
private final ClassLoader parent;
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
...
在面向对象编程中,有一条非常经典的设计原则:
组合优于继承,多用组合少用继承。
双亲委派模型的执行流程
双亲委派模型的实现代码非常简单,逻辑非常清晰,都集中在
java.lang.ClassLoader
的
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) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
} catch (ClassNotFoundException e) {
if (c == null) {
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) {
resolveClass(c);
return c;