# 类加载过程详解

# 类的生命周期

类从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期可以简单概括为 7 个阶段:

  • 加载(Loading):查找并加载类的二进制数据
  • 连接/链接(Linking) — 细分为3个阶段:
    • 验证(Verification):确保被加载的类的正确性
    • 准备(Preparation):为类的静态变量分配内存,并将其初始化为默认值
    • 解析(Resolution):把类中的符号引用转换为直接引用
  • 初始化(Initialization)
  • 使用(Using)
  • 卸载(Unloading)

一个类的完整生命周期

# 类加载过程

class文件 需要加载到虚拟机中之后才能运行和使用,那么虚拟机是如何加载这些 class文件 呢?

系统加载 Class 类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析

类加载过程

# Step 1 | 加载/Loading

加载(Loading)是「类加载过程」的第一步,主要完成下面 3 件事情:

  1. 通过类的全限定名获取定义此类的二进制字节流。
  2. 将字节流所代表的 静态存储结构 转换为 方法区的运行时数据结构
  3. 在内存(Java堆)中生成一个代表该类的「Class对象」,作为方法区中这些数据的访问入口。

TIP

类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它。

如果在预先加载的过程中遇到了「.class文件」缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误),如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。

加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。

# Step 2 | 连接/Linking

# Step 2.1 | 验证/Verification

验证(Verification)是连接阶段的第一步,这一阶段的目的是确保「.class文件」的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。

TIP

验证阶段这一步在整个类加载过程中耗费的资源还是相对较多的,但很有必要,可以有效防止恶意代码的执行。任何时候,程序安全都是第一位。

不过,验证阶段也不是必须要执行的阶段。如果程序运行的全部代码(包括自己编写的、第三方包中的、从外部加载的、动态生成的等所有代码)都已经被反复使用和验证过,在生产环境的实施阶段就可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

验证阶段主要由4个检验阶段组成:

  • 文件格式验证(「.class文件」格式检查)
  • 元数据验证(字节码语义检查)
  • 字节码验证(程序语义检查)
  • 符号引用验证(类的正确性检查)

验证阶段示意图

值得注意的是:

  • 「文件格式验证」这一阶段是基于该类的二进制字节流进行的,主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java 类型信息的要求。
  • 除了这一阶段之外,其余三个验证阶段都是基于方法区的存储结构上进行的,不会再直接读取、操作字节流了。

「符号引用验证」发生在类加载过程中的「解析阶段」,具体点说是 JVM 将「符号引用」转化为「直接引用」 」的时候(「解析阶段」会介绍「符号引用」和「直接引用」)。

「符号引用验证」的主要目的是确保「解析阶段」能正常执行,如果无法通过「符号引用验证」,JVM 会抛出异常,比如:

  • java.lang.IllegalAccessError:当类试图访问或修改它没有权限访问的字段,或调用它没有权限访问的方法时,抛出该异常。
  • java.lang.NoSuchFieldError:当类试图访问或修改一个指定的对象字段,而该对象不再包含该字段时,抛出该异常。
  • java.lang.NoSuchMethodError:当类试图访问一个指定的方法,而该方法不存在时,抛出该异常。
  • ……
# Step 2.2 | 准备/Preparation

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

  1. 这时候进行内存分配的仅包括类变量(Class Variables ,即静态变量,被 static 关键字修饰的变量,只与类相关,因此被称为类变量),而不包括实例变量。实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
  2. 这里所设置的初始值"通常情况"下是数据类型默认的零值(如 00Lnullfalse 等)
    • 比如我们定义了 public static int value=111 ,那么 value 变量在准备阶段的初始值就是 0 而不是 111(初始化阶段才会赋值)。
    • 特殊情况:比如给 value 变量加上了 final 关键字 public static final int value=111 ,那么准备阶段 value 的值就被赋值为 111
  3. 从概念上讲,类变量所使用的内存都应当在方法区中进行分配。不过有一点需要注意的是:
    • JDK 7 之前,HotSpot 使用永久代来实现方法区的时候,实现是完全符合这种逻辑概念的。
    • 而在 JDK 7 及之后,HotSpot 已经把原本放在永久代的字符串常量池、静态变量等移动到堆中,这个时候类变量则会随着 Class 对象一起存放在 Java 堆中。
# Step 2.3 | 解析/Resolution

解析阶段是虚拟机将常量池内的「符号引用」替换为「直接引用」的过程。 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类「符号引用」进行。

《深入理解 Java 虚拟机》7.34 节第三版对「符号引用」和「直接引用」的解释如下:

符号引用和直接引用

举个例子:在程序执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法了。通过解析操作「符号引用」就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。

综上,解析阶段是虚拟机将常量池内的「符号引用」替换为「直接引用」的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量

# Step 3 | 初始化/Initialization

初始化阶段是执行初始化方法 <clinit>() 的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。

说明:<clinit>() 方法是编译之后自动生成的。

对于 <clinit>() 方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为 <clinit>() 方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起多个线程阻塞,并且这种阻塞很难被发现。

对于初始化阶段,虚拟机严格规范了有且只有 6 种情况下,必须对类进行初始化(只有主动去使用类才会初始化类):

  1. 当遇到 newgetstaticputstaticinvokestatic 这 4 条字节码指令时,比如 new 一个类、读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
    • 当 JVM 执行 new 指令时会初始化类。即当程序创建一个类的实例对象。
    • 当 JVM 执行 getstatic 指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。
    • 当 JVM 执行 putstatic 指令时会初始化类。即程序给类的静态变量赋值。
    • 当 JVM 执行 invokestatic 指令时会初始化类。即程序调用类的静态方法。
  2. 使用 java.lang.reflect 包的方法对类进行反射调用时如 Class.forName("..."), newInstance() 等等。如果类没初始化,需要触发其初始化。
  3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
  4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
  5. MethodHandleVarHandle 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用, 就必须先使用 findStaticVarHandle 来初始化要调用的类。
  6. 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

# 类卸载

卸载类即该类的「Class对象」被 GC。

卸载类需要满足 3 个要求:

  1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
  2. 该类没有在其他任何地方被引用
  3. 该类的类加载器的实例已被 GC

所以,在 JVM 生命周期内,由 JVM 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。

只要想通一点就好了,JDK 自带的 BootstrapClassLoader, ExtClassLoader, AppClassLoader 负责加载 JDK 提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。

TIP

Java虚拟机将结束生命周期的几种情况:

  • 执行了 System.exit()方法
  • 程序正常执行结束
  • 程序在执行过程中遇到了异常或错误而异常终止
  • 由于操作系统出现错误而导致Java虚拟机进程终止

# 类加载器

# 类加载器介绍

「类加载器」从 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 是一致的。

从上面的介绍可以看出:

  • 「类加载器」是一个负责加载类的对象,用于实现「类加载过程」中的「加载/Loading」这一步。
  • 每个 Java 类都有一个引用指向加载它的 ClassLoader
  • 数组类不是通过 ClassLoader 创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的。
class Class<T> {
  ...
  private final ClassLoader classLoader;
  @CallerSensitive
  public ClassLoader getClassLoader() {
     //...
  }
  ...
}
1
2
3
4
5
6
7
8
9
1
2
3
4
5
6
7
8
9

简单来说,类加载器的主要作用就是加载 Java 类的字节码(「.class文件」)到 JVM 中(在内存中生成一个代表该类的「Class对象」)。 字节码可以是 Java 源程序(.java文件)经过 javac 编译得来,也可以是通过工具动态生成或者通过网络下载得来。

其实除了加载类之外,类加载器还可以加载 Java 应用所需的资源如文本、图像、配置文件、视频等等文件资源。本文只讨论其核心功能:加载类

# 类加载器的加载规则

JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加友好。

对于已经加载的类会被放在 ClassLoaderclasses 中。在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。

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
3
4
5
6
7
8
9
10
11
1
2
3
4
5
6
7
8
9
10
11

# 类加载器的层次

类加载器层次关系图

注意:

这里父类加载器并不是通过继承关系来实现的,而是采用组合实现的。

# 正常:从Java开发人员的角度来看

站在Java开发人员的角度来看,类加载器可以大致划分为三类,这也是 JVM 中内置的三个重要 ClassLoader

  1. BootstrapClassLoader(启动类加载器):最顶层的加载类,由 C++ 实现,通常表示为 null,并且没有父级,主要用来加载 JDK 内部的核心类库( %JAVA_HOME%/jre/lib 目录下的 rt.jarresources.jarcharsets.jar 等 jar 包和类)以及被 -Xbootclasspath 参数指定的路径下的所有类。启动类加载器是无法被Java程序直接引用的。
  2. ExtensionClassLoader(扩展类加载器):主要负责加载 %JAVA_HOME%/jre/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类。开发者可以直接使用扩展类加载器。
  3. AppClassLoader(应用程序类加载器):面向我们用户的加载器,该类加载器由sun.misc.Launcher$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文件」)进行加密,加载时再利用自定义的类加载器对其解密。
  • 在执行非置信代码之前,自动验证数字签名。
  • 动态地创建符合用户特定需要的定制化构建类。
  • 从特定的场所取得 java class,例如数据库中和网络中。

# 特殊:从Java虚拟机的角度来看

站在Java虚拟机的角度来讲,只存在两种不同的类加载器:

  1. 启动类加载器 BootstrapClassLoader: 它使用 C++ 实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分;
  2. 所有其他的类加载器: 这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类 java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。

也就是说,除了 BootstrapClassLoader 是 JVM 自身的一部分(C++ 语言实现)之外,其他所有的类加载器都是在 JVM 外部实现的(Java语言实现),并且全都继承自 ClassLoader 抽象类

# 类加载器层次的例子

每个 ClassLoader 可以通过 getParent() 获取其父 ClassLoader,如果获取到 ClassLoadernull 的话,那么该类是通过 BootstrapClassLoader 加载的。

🐎 例子:

package com.pdai.jvm.classloader;
public class ClassLoaderTest {
     public static void main(String[] args) {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        System.out.println(loader);
        System.out.println(loader.getParent());
        System.out.println(loader.getParent().getParent());
    }
}
1
2
3
4
5
6
7
8
9
1
2
3
4
5
6
7
8
9

🗨 结果如下:

  • 我们编写的 Java 类 ClassLoaderTestClassLoaderAppClassLoader
  • AppClassLoader 的 父ClassLoaderExtClassLoader
  • ExtClassLoader 的 父ClassLoaderBootstrapClassLoader,因此输出结果为 null
sun.misc.Launcher$AppClassLoader@64fef26a
sun.misc.Launcher$ExtClassLoader@1ddd40f3
null
1
2
3
1
2
3

# 自定义类加载器

我们前面也说说了,除了 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):根据类的二进制名称来查找类,默认实现是空方法。

如果我们不想打破双亲委派机制,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派机制,则需要重写 loadClass() 方法。

🌰自定义加载器示例

下面我们通过一个示例,来演示自定义类加载器,只重写 findClass() 方法的流程:

package com.pdai.jvm.classloader;
import java.io.*;

public class MyClassLoader extends ClassLoader {

    private String root;

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] loadClassData(String className) {
        String fileName = root + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
        try {
            InputStream ins = new FileInputStream(fileName);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
            int length = 0;
            while ((length = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, length);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public String getRoot() {
        return root;
    }

    public void setRoot(String root) {
        this.root = root;
    }

    public static void main(String[] args)  {

        MyClassLoader classLoader = new MyClassLoader();
        classLoader.setRoot("D:\\temp");

        Class<?> testClass = null;
        try {
            testClass = classLoader.loadClass("com.pdai.jvm.classloader.Test2");
            Object object = testClass.newInstance();
            System.out.println(object.getClass().getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密。由于这里只是演示,我并未对class文件进行加密,因此没有解密的过程。

这里有几点需要注意:

  1. 这里传递的文件名需要是类的全限定性名称,即com.pdai.jvm.classloader.Test2格式的,因为 defineClass 方法是按这种格式进行处理的。
  2. 这类 Test 类本身可以被 AppClassLoader 类加载,因此我们不能把 com/pdai/jvm/classloader/Test2.class 放在类路径下。否则,由于双亲委托机制的存在,会直接导致该类由 AppClassLoader 加载,而不会通过我们自定义类加载器来加载。
  3. 如果不想破坏双亲委托机制,最好不要重写 loadClass() 方法。

# JVM类加载机制

从前面的知识中我们知道,类加载器有很多种,当我们想要加载一个类的时候,具体是使用哪个类加载器来加载呢? 这就需要提到双亲委派机制了。

# 双亲委派机制-介绍

根据官网介绍:

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 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。

注意⚠️:类加载器之间的父子关系一般不是以继承的关系来实现的,而是通常使用组合关系来复用父加载器的代码。

public abstract class ClassLoader {
  ...
  // 组合
  private final ClassLoader parent;
  protected ClassLoader(ClassLoader parent) {
       this(checkCreateClassLoader(), parent);
  }
  ...
}
1
2
3
4
5
6
7
8
9
1
2
3
4
5
6
7
8
9

在面向对象编程中,有一条非常经典的设计原则:组合优于继承,多用组合少用继承。

# 双亲委派机制-执行流程

# 简洁版

简洁版:双亲委派机制的过程

  1. AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
  2. ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
  3. 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
  4. ExtClassLoader也加载失败,则会使用AppClassLoader来加载
  5. 如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException
# 结合代码解释版

双亲委派机制的实现代码非常简单,逻辑非常清晰,都集中在 java.lang.ClassLoaderloadClass() 中,相关代码如下所示:

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;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

结合上面的源码,简单总结一下双亲委派机制的执行流程:

  • 在类加载的时候,系统会首先判断当前类是否被加载过 findLoadedClass(name)。已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。
  • 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 parent.loadClass()方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器 BootstrapClassLoader 中。
  • 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的 findClass() 方法来加载类)。
  • 如果子类加载器也无法加载这个类,那么它会抛出一个 ClassNotFoundException 异常。

# 双亲委派机制-好处

  • 对于系统类,防止内存中出现多份同样的字节码
  • 保证Java程序安全稳定运行

双亲委派机制保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。

如果没有使用双亲委派机制,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现两个不同的 Object 类。双亲委派机制可以保证加载的是 JRE 里的那个 Object 类,而不是你写的 Object 类。这是因为 AppClassLoader 在加载你的 Object 类时,会委托给 ExtClassLoader 去加载,而 ExtClassLoader 又会委托给 BootstrapClassLoaderBootstrapClassLoader 发现自己已经加载过了 Object 类,会直接返回,不会去加载你写的 Object 类。

# 双亲委派机制-打破方法

双亲委派机制并不是一种强制性的约束,只是 JDK 官方推荐的一种方式。如果我们因为某些特殊需求想要打破双亲委派机制,也是可以的。

自定义加载器的话,需要继承 ClassLoader如果我们不想打破双亲委派机制,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派机制则需要重写 loadClass() 方法

为什么是重写 loadClass() 方法打破双亲委派机制呢?双亲委派机制的执行流程已经解释了:

类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass() 方法来加载类)。

重写 loadClass()方法之后,我们就可以改变传统双亲委派机制的执行流程。例如,子类加载器可以在委派给父类加载器之前,先自己尝试加载这个类,或者在父类加载器返回之后,再尝试从其他地方加载这个类。具体的规则由我们自己实现,根据项目需求定制化。

我们比较熟悉的 Tomcat 服务器为了能够优先加载 Web 应用目录下的类,然后再加载其他目录下的类,就自定义了类加载器 WebAppClassLoader 来打破双亲委托机制。这也是 Tomcat 下 Web 应用之间的类实现隔离的具体原理。

Tomcat 的类加载器的层次结构如下:

Tomcat 的类加载器的层次结构

Tomcat 这四个自定义的类加载器对应的目录如下:

  • CommonClassLoader 对应 <Tomcat>/common/*
  • CatalinaClassLoader 对应 <Tomcat>/server/*
  • SharedClassLoader 对应 <Tomcat>/shared/*
  • WebAppClassloader 对应 <Tomcat>/webapps/<app>/WEB-INF/*