文章

Java 类加载器的双亲委派模型

ClassLoader

ClassLoader 就是类加载器,它的作用就是根据指定全限定名称将 class 文件加载到 JVM 内存中,转为 Class 对象。

ClassLoader 的分类

从 JVM 角度来说,ClassLoader 可以分为两类:

  1. Bootstrap ClassLoader(启动类加载器)

    这个类加载器由 C++ 实现,负责将存放在 \lib 目录或 -Xbootclasspath 参数指定的路径中的类库加载到内存中,与 JVM 是一体的。

  2. 其他类加载器

    其他类加载器又可以分为以下三类

    1. Extension ClassLoader(扩展类加载器)

      负责加载 \lib\ext 目录或 java.ext.dirs 系统变量指定的路径中的所有类库。

    2. Application ClassLoader(应用程序类加载器)

      负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果没有自定义类加载器默认就是用这个加载器。

    3. User ClassLoader(用户自定义类加载器)

      如果需要自己控制类加载过程,可以自己实现类加载器,一般很少用到。

双亲委派模型

classloader

工作过程

双亲委派模型的工作过程是这样的:

  1. 如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成,每个类加载器都是如此,直到最上层的 Bootstrap ClassLoader。(由下到上)
  2. 当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),会往下传递至子加载器,子加载器才会尝试自己去加载。(由上到下)

为什么要使用双亲委派模型

  1. 确保一个类只会被加载一次,防止重复加载

    因为每个类需要加载的时候都会先由子加载器委派给父加载器加载,如果某一层级的加载器查找到了该类已经加载,那么类加载器就会返回该类,而不会重新加载一个新类。

  2. 建立信任层级,防止恶意代码加载

    一个例子说明:用户自己编写了一个类 java.lang.Object,与系统的 java.lang.Object “同名同姓”,当需要加载一个全限定类名为 java.lang.Object 的类时候,由于使用了双亲委派模型,会由最上层的 Bootstrap ClassLoader 先加载系统的 java.lang.Object 类,并且子加载器不会再加载任何全限定名为 java.lang.Object 的类,也就是说用户自己的编写的 Object 类就不会被加载进 JVM,而如果没有双亲委派模型,那么通过自定义类加载器可以将用户编写的 java.lang.Object 加载进 JVM,而 JVM 就可能误以为用户自定义的 java.lang.Object 就是系统的 java.lang.Object,导致“恶意代码”被执行。这个例子可以理解为通过双亲委派模型建立起了多层信任层级,优先级最高的当然是 Bootstrap ClassLoader,接着是 Extension ClassLoader,再往下是 Application ClassLoader,最后才是用户自定义的类加载器。

  3. 类加载器不同,相同全限定类名的两个类并不是同一个类型

    在自定义的类加载器中,可以强制加载某个类,例如上文提到的用户自己编写的 java.lang.Object 类,但是即使这样,“恶意代码”也不会被执行,因为在JVM 中,判断一个对象是否是某个类型时,如果该对象的实际类型与待比较的类型的类加载器不同,那么会返回false。也就是说通过自定义类加载器强制加载的java.lang.Object 与 Bootstrap ClassLoader 加载的 java.lang.Object 并不是同一个类型,所以不会导致“恶意代码”被执行。

实现双亲委派模型

父子加载器之间并不是通过继承实现的,而是而是使用组合包含关系,通过 ClassLoader 源码可以得知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class ClassLoader {

    private static native void registerNatives();
    static {
        registerNatives();
    }

    // The parent class loader for delegation
    // Note: VM hardcoded the offset of this field, thus all new fields
    // must be added *after* it.
    // 父类加载器
    private final ClassLoader parent;
	...
}

ClassLoader 实现双亲委派模型的几个重要方法

loadClass()

loadClass()的默认实现如下:

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
public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
}

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,调用 findLoadedClass() 方法检查类是否已经被加载
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 如果父加载器不为 null,委派给父加载器进行加载,否则寻找启动类加载器进行加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        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();
                    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(c);
            }
            return c;
        }
    }

从上面的源码可以看到双亲委派模型就是通过 loadClass(String name, boolean resolve) 方法实现的,实现过程大致如下:

  1. 首先,调用 findLoadedClass() 方法检查类是否已经被加载,如果已经被加载,直接返回。
  2. 如果没有被加载过并且父加载器不为 null,则委派给父加载器进行加载,否则寻找启动类加载器进行加载。
  3. 如果父加载器及启动类加载器都没有找到指定的类,那么调用当前类加载器的 findClass() 方法来完成类加载。

通过对 loadClass() 方法的分析,我们可以知道,如果要自定义类加载器,那么就必须重写 findClass() 方法。

findClass()

findClass() 的默认实现就是抛出一个 ClassNotFoundException

1
2
3
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

如果要自定义类加载器,那么我们需要做的工作就是重写 findClass() 方法,在这个方法里面实现将一个指定类名称转换为 Class 对象。ClassLoader 类中提供了一个 defineClass() 方法,通过这个方法,就可以把一个字节数组转为Class对象,十分简单。

defineClass()

这个方法的作用是将一个字节数组转为 Class 对象,这个字节数组是 class 文件读取后最终的字节数组。

实现自定义类加载器

通过上面对 ClassLoader 源码中实现双亲委派模型的几个重要方法的分析,我们可以知道实现自定义类加载器大致需要以下三个步骤:

  1. 自定义一个类(MyClassLoader)继承 ClassLoader 抽象类;
  2. 重写 ClassLoader 的 findClass(String name) 方法;
  3. 在 findClass(String name) 中实现将一个指定类名称转换为 Class 对象,即调用 defineClass() 方法。

示例

自定义一个 ClassLoader 加载某个 class 文件

  1. 创建类 ClassLoaderTest 编译生成 ClassLoaderTest.class 文件,删除 ClassLoaderTest.java 文件

    1
    2
    3
    4
    5
    6
    7
    
     package cc.white.classloaderdemo;
    
     public class ClassLoaderTest {
         public void helloWorld() {
             System.out.println("hello classloader");
         }
     }
    
  2. 创建自定义类加载器 MyClassLoader 继承 ClassLoader,重写 findClass() 方法,在 findClass() 中调用 defineClass() 方法实现类加载

    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
    
     package cc.white.classloaderdemo;
    
     import java.io.ByteArrayOutputStream;
     import java.io.File;
     import java.io.FileInputStream;
     import java.io.IOException;
    
     /**
     * 自定义类加载器加载 myclass/ClassLoaderTest.class
     */
     public class MyClassLoader extends ClassLoader {
    
         @Override
         protected Class<?> findClass(String name) throws ClassNotFoundException {
             FileInputStream fileInputStream = null;
             ByteArrayOutputStream byteArrayOutputStream = null;
             try {
                 fileInputStream = new FileInputStream(new File("myclass/ClassLoaderTest.class"));
                 byteArrayOutputStream = new ByteArrayOutputStream();
                 int len;
                 try {
                     while ((len = fileInputStream.read()) != -1) {
                         byteArrayOutputStream.write(len);
                     }
                 } catch (IOException e) {
                     e.printStackTrace();
                 }
    
                 byte[] data = byteArrayOutputStream.toByteArray();
    
                 return defineClass(name, data, 0, data.length);
             } catch (IOException e) {
                 e.printStackTrace();
             } finally {
                 if (fileInputStream != null) {
                     try {
                         fileInputStream.close();
                     } catch (IOException e) {
                         e.printStackTrace();
                     }
                 }
                 if (byteArrayOutputStream != null) {
                     try {
                         byteArrayOutputStream.close();
                     } catch (IOException e) {
                         e.printStackTrace();
                     }
                 }
             }
             return super.findClass(name);
         }
     }
    
    
  3. 测试 MyClassLoader 能否使用

    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
    
     package cc.white.classloaderdemo;
    
     import java.lang.reflect.InvocationTargetException;
     import java.lang.reflect.Method;
    
     public class Main {
    
         public static void main(String[] args) {
             MyClassLoader myClassLoader = new MyClassLoader();
             try {
                 // 加载全限定类名为 cc.white.classloaderdemo.ClassLoaderTest 的 class 文件
                 Class c = myClassLoader.loadClass("cc.white.classloaderdemo.ClassLoaderTest");
                 if (c != null) {
                     try {
                         Object obj = c.newInstance();
                         Method method = c.getDeclaredMethod("helloWorld", null);
                         //通过反射调用 Test 类的 helloWorld 方法
                         method.invoke(obj, null);
                     } catch (IllegalAccessException | InvocationTargetException | InstantiationException | NoSuchMethodException e) {
                         e.printStackTrace();
                     }
                 }
             } catch (ClassNotFoundException e) {
                 e.printStackTrace();
             }
         }
     }
    
    

测试代码执行结果如下:

1
hello classloader

输出 hello classloader 说明正确加载了 cc.white.classloaderdemo.ClassLoaderTest 类,并执行了 helloWorld 方法。

总结

Java 的类加载器是用来加载 class 文件到 JVM 内存中的,可以分为两大类:Bootstrap ClassLoader(启动类加载器)和其他类加载器,而其他类加载器大致有以下三种:Extension ClassLoader(扩展类加载器)、Application ClassLoader(应用程序类加载器)、User ClassLoader(用户自定义类加载器)。类加载器之间存在父子关系,加载一个类的时候会使用双亲委派模型进行加载,每个子加载器都会先将加载需求委托给上级父加载器,父加载器搜索不到该类的时候再往下传递由子加载器进行加载。使用双亲委派模型可以有效的防止“恶意代码”被注入执行。

通过分析 ClassLoader 源码,了解了双亲委派模型的实现方式,以及实现自定义 ClassLoader 的基本步骤,主要依赖几个方法:loadClass()findClass()defineClass()

本文由作者按照 CC BY 4.0 进行授权