深入分析 Java 类加载器原理

本贴最后更新于 1954 天前,其中的信息可能已经物是人非

🌹🌹 如果您觉得我的文章对您有帮助的话,记得在 GitHub 上 star 一波哈 🌹🌹

🌹🌹GitHub_awesome-it-blog 🌹🌹


本文分析了双亲委派模型的实现原理,并通过代码示例说明了什么时候需要实现自己的类加载器以及如何实现自己的类加载器。

本文基于 JDK8。

0 ClassLoader 的作用

ClassLoader 用于将 class 文件加载到 JVM 中。另外一个作用是确认每个类应该由哪个类加载器加载。

第二个作用也用于判断 JVM 运行时的两个类是否相等,影响的判断方法有 equals()、isAssignableFrom()、isInstance()以及 instanceof 关键字,这一点在后文中会举例说明。

0.1 何时出发类加载动作?

类加载的触发可以分为隐式加载和显示加载。

隐式加载

隐式加载包括以下几种情况:

  • 遇到 new、getstatic、putstatic、invokestatic 这 4 条字节码指令时
  • 对类进行反射调用时
  • 当初始化一个类时,如果其父类还没有初始化,优先加载其父类并初始化
  • 虚拟机启动时,需指定一个包含 main 函数的主类,优先加载并初始化这个主类

显示加载

显示加载包含以下几种情况:

  • 通过 ClassLoader 的 loadClass 方法
  • 通过 Class.forName
  • 通过 ClassLoader 的 findClass 方法

0.2 被加载的类存放在哪里

JDK8 之前会加载到内存中的方法区。
从 JDK8 到现在为止,会加载到元数据区。

1 都有哪些 ClassLoader

整个 JVM 平台提供三类 ClassLoader。

1.1 Bootstrap ClassLoader

加载 JVM 自身工作需要的类,它由 JVM 自己实现。它会加载 $JAVA_HOME/jre/lib 下的文件

1.2 ExtClassLoader

它是 JVM 的一部分,由 sun.misc.LauncherExtClassLoader实现,他会加载JAVA_HOME/jre/lib/ext 目录中的文件(或由 System.getProperty("java.ext.dirs")所指定的文件)。

1.3 AppClassLoader

应用类加载器,我们工作中接触最多的也是这个类加载器,它由 sun.misc.Launcher$AppClassLoader 实现。它加载由 System.getProperty("java.class.path")指定目录下的文件,也就是我们通常说的 classpath 路径。

2 双亲委派模型

2.1 双亲委派模型原理

从 JDK1.2 之后,类加载器引入了双亲委派模型,其模型图如下:

Classloader.jpg

其中,两个用户自定义类加载器的父加载器是 AppClassLoader,AppClassLoader 的父加载器是 ExtClassLoader,ExtClassLoader 是没有父类加载器的,在代码中,ExtClassLoader 的父类加载器为 null。BootstrapClassLoader 也并没有子类,因为他完全由 JVM 实现。

双亲委派模型的原理是:当一个类加载器接收到类加载请求时,首先会请求其父类加载器加载,每一层都是如此,当父类加载器无法找到这个类时(根据类的全限定名称),子类加载器才会尝试自己去加载。

为了说明这个继承关系,我这里实现了一个自己的类加载器,名为 TestClassLoader,在类加载器中,用 parent 字段来表示当前加载器的父类加载器,其定义如下:

public abstract class ClassLoader {
...
    // 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;
...
}

然后通过 debug 来看一下这个结构,如下图

ClassLoader 委派模型关系图

这里的第一个红框是我自己定义的类加载器,对应上图的最下层部分;第二个框是自定义类加载器的父类加载器,可以看到是 AppClassLoader;第三个框是 AppClassLoader 的父类加载器,是 ExtClassLaoder;第四个框是 ExtClassLoader 的父类加载器,是 null。

OK,这里先有个直观的印象,后面的实现原理中会详细介绍。

2.2 此模型解决的问题

为什么要使用双亲委派模型呢?它可以解决什么问题呢?

双亲委派模型是 JDK1.2 之后引入的。根据双亲委派模型原理,可以试想,没有双亲委派模型时,如果用户自己写了一个全限定名为 java.lang.Object 的类,并用自己的类加载器去加载,同时 BootstrapClassLoader 加载了 rt.jar 包中的 JDK 本身的 java.lang.Object,这样内存中就存在两份 Object 类了,此时就会出现很多问题,例如根据全限定名无法定位到具体的类。

有了双亲委派模型后,所有的类加载操作都会优先委派给父类加载器,这样一来,即使用户自定义了一个 java.lang.Object,但由于 BootstrapClassLoader 已经检测到自己加载了这个类,用户自定义的类加载器就不会再重复加载了。

所以,双亲委派模型能够保证类在内存中的唯一性。

2.3 双亲委派模型实现原理

下面从源码的角度看一下双亲委派模型的实现。

JVM 在加载一个 class 时会先调用 classloader 的 loadClassInternal 方法,该方法源码如下

// This method is invoked by the virtual machine to load a class.
private Class<?> loadClassInternal(String name)
    throws ClassNotFoundException
{
    // For backward compatibility, explicitly lock on 'this' when
    // the current class loader is not parallel capable.
    if (parallelLockMap == null) {
        synchronized (this) {
             return loadClass(name);
        }
    } else {
        return loadClass(name);
    }
}

该方法里面做的事儿就是调用了 loadClass 方法,loadClass 方法的实现如下

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        // 先查看这个类是否已经被自己加载了
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 如果有父类加载器,先委派给父类加载器来加载
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // 如果父类加载器为null,说明ExtClassLoader也没有找到目标类,则调用BootstrapClassLoader来查找
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
            // 如果都没有找到,调用findClass方法,尝试自己加载这个类
            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;
    }
}

源码中已经给出了几个关键步骤的说明。

代码中调用 BootstrapClassLoader 的地方实际是调用的 native 方法。

由此可见,双亲委派模型实现的核心就是这个 loadClass 方法。

3 实现自己的类加载器

3.1 为什么要实现自己的类加载器

回答这个问题首先要思考类加载器有什么作用(粗体标出)。

3.1.1 类加载器的作用

类加载器有啥作用呢?我们再回到上面的源码。

从上文我们知道 JVM 通过 loadClass 方法来查找类,所以,他的第一个作用也是最重要的:在指定的路径下查找 class 文件(各个类加载器的扫描路径在上文已经给出)。

然后,当父类加载器都说没有加载过目标类时,他会尝试自己加载目标类,这就调用了 findClass 方法,可以看一下 findClass 方法的定义:

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

可以发现他要求返回一个 Class 对象实例,这里我通过一个实现类 sun.rmi.rmic.iiop.ClassPathLoader 来说明一下 findClass 都干了什么。

protected Class findClass(String var1) throws ClassNotFoundException {
    // 从指定路径加载指定名称的class的字节流
    byte[] var2 = this.loadClassData(var1);
    // 通过ClassLoader的defineClass来创建class对象实例
    return this.defineClass(var1, var2, 0, var2.length);
}

他做的事情在注释中已经给出,可以看到,最终是通过 defineClass 方法来实例化 class 对象的。

另外可以发现,class 文件字节的获取和处理我们是可以控制的。所以,第二个作用:我们可以在字节流解析这一步做一些自定义的处理。 例如,加解密。

接下来,看似还有个 defineClass 可以让我们来做点儿什么,ClassLoader 的实现如下:

protected final Class<?> defineClass(String name, byte[] b, int off, int len)
    throws ClassFormatError
{
    return defineClass(name, b, off, len, null);
}

被 final 掉了,没办法覆写,所以这里看似不能做什么事儿了。

小结一下:

  • 通过 loadClass 在指定的路径下查找文件。
  • 通过 findClass 方法解析 class 字节流,并实例化 class 对象。
3.1.2 什么时候需要自己实现类加载器

当 JDK 提供的类加载器实现无法满足我们的需求时,才需要自己实现类加载器。

现有应用场景:OSGi、代码热部署等领域。

另外,根据上述类加载器的作用,可能有以下几个场景需要自己实现类加载器

  • 当需要在自定义的目录中查找 class 文件时(或网络获取)
  • class 被类加载器加载前的加解密(代码加密领域)

3.2 如何实现自己的类加载器

接下来,实现一个在自定义 class 类路径中查找并加载 class 的自定义类加载器。

实验中的 ClassLoaderTest 类就是一个简单的定义了两个 field 的 Class。如下图所示

classloadertest.jpg

最终的打印结果如下

classloaderresult.jpg

PS: 实验类(ClassLoaderTest)最好不要放在 IDE 的工程目录内,因为 IDE 在 run 的时候会先将工程中的所有类都加载到内存,这样一来这个类就不是自定义类加载器加载的了,而是 AppClassLoader 加载的。

3.3 类加载器对“相等”判断的影响

3.3.1 对 Object.equals()的影响

还是上面那个自定义类加载器

修改 MyClassLoader 代码

3.3.2 对 instanceof 的影响

修改 TestClassLoader,增加 main 方法来实验,修改后的 TestClassLoader 如下:

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

/**
 * 自定义ClassLoader
 * 功能:可自定义class文件的扫描路径
 * @author zhiminxu
 */
// 继承ClassLoader,获取基础功能
public class TestClassLoader extends ClassLoader {

    public static void main(String[] args) throws ClassNotFoundException {
        TestClassLoader testClassLoader = new TestClassLoader("/Users/zhiminxu/developer/classloader");
        Object obj = testClassLoader.loadClass("ClassLoaderTest");
        // obj是testClassLoader加载的,ClassLoaderTest是AppClassLoader加载的,所以这里打印:false
        System.out.println(obj instanceof ClassLoaderTest);
    }

    // 自定义的class扫描路径
    private String classPath;

    public TestClassLoader(String classPath) {
        this.classPath = classPath;
    }

    // 覆写ClassLoader的findClass方法
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // getDate方法会根据自定义的路径扫描class,并返回class的字节
        byte[] classData = getDate(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            // 生成class实例
            return defineClass(name, classData, 0, classData.length);
        }
    }


    private byte[] getDate(String name) {
        // 拼接目标class文件路径
        String path = classPath + File.separatorChar + name.replace('.', File.separatorChar) + ".class";
        try {
            InputStream is = new FileInputStream(path);
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            byte[] buffer = new byte[2048];
            int num = 0;
            while ((num = is.read(buffer)) != -1) {
                stream.write(buffer, 0 ,num);
            }
            return stream.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

打印结果:

false

其他的还会影响 Class 的 isAssignableFrom()方法和 isInstance()方法,原因与上面的相同。

3.3.3 另外的说明

不要尝试自定义 java.lang 包,并尝试用加载器去加载他们。像下面这样

selfclass.jpg

这么干的话,会直接抛出一个异常

javalangerror.jpg

这个异常是在调用 defineClass 的校验过程抛出的,源码如下

4 跟 ClassLoader 相关的几个异常

想知道如下几个异常在什么情况下抛出,其实只需要在 ClassLoader 中找到哪里会抛出他,然后在看下相关逻辑即可。

4.1 ClassNotFoundException

这个异常,相信大家经常遇到。

那么,到底啥原因导致抛出这个异常呢?

看一下 ClassLoader 的源码,在 JVM 调用 loadClassInternal 的方法中,就会抛出这个异常。

其声明如下:

// This method is invoked by the virtual machine to load a class.
private Class<?> loadClassInternal(String name)
    throws ClassNotFoundException
{
    // For backward compatibility, explicitly lock on 'this' when
    // the current class loader is not parallel capable.
    if (parallelLockMap == null) {
        synchronized (this) {
             return loadClass(name);
        }
    } else {
        return loadClass(name);
    }
}

这里面 loadClass 方法会抛出这个异常,再来看 loadClass 方法

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}

调用了重写的 loadClass 方法

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        // 查看findLoadedClass的声明,没有抛出这个异常
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    // 这里会抛,但同样是调loadClass方法,无需关注
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // catch住没有抛,因为要在下面尝试自己获取class
                // 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;
    }
}

再来看 findClass 方法

/**
 * Finds the class with the specified <a href="#name">binary name</a>.
 * This method should be overridden by class loader implementations that
 * follow the delegation model for loading classes, and will be invoked by
 * the {@link #loadClass <tt>loadClass</tt>} method after checking the
 * parent class loader for the requested class.  The default implementation
 * throws a <tt>ClassNotFoundException</tt>.
 *
 * @param  name
 *         The <a href="#name">binary name</a> of the class
 *
 * @return  The resulting <tt>Class</tt> object
 *
 * @throws  ClassNotFoundException
 *          If the class could not be found
 *
 * @since  1.2
 */
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

果然这里抛出的,注释中抛出这个异常的原因是说:当这个 class 无法被找到时抛出。

这也就是为什么在上面自定义的类加载器中,覆写 findClass 方法时,如果没有找到 class 要抛出这个异常的原因。

至此,这个异常抛出的原因就明确了:在双亲委派模型的所有相关类加载器中,目标类在每个类加载器的扫描路径中都不存在时,会抛出这个异常。

4.2 NoClassDefFoundError

这也是个经常会碰到的异常,而且不熟悉的同学可能经常搞不清楚什么时候抛出 ClassNotFoundException,什么时候抛出 NoClassDefFoundError。

我们还是到 ClassLoader 中搜一下这个异常,可以发现在 defineClass 方法中可能抛出这个异常,defineClass 方法源码如下:

这个校验的源码如下

// true if the name is null or has the potential to be a valid binary name
private boolean checkName(String name) {
    if ((name == null) || (name.length() == 0))
        return true;
    if ((name.indexOf('/') != -1)
        || (!VM.allowArraySyntax() && (name.charAt(0) == '[')))
        return false;
    return true;
}

所以,这个异常抛出的原因是:类名校验未通过。

但这个异常其实不止在 ClassLoader 中抛出,其他地方,例如框架中,web 容器中都有可能抛出,还要具体问题具体分析。

5 总结

本文先介绍了 ClassLoader 的作用:主要用于从指定路径查找 class 并加载到内存,另外是判断两个类是否相等。

后面介绍了双亲委派模型,以及其实现原理,JDK 中主要是在 ClassLoader 的 loadClass 方法中实现双亲委派模型的。

然后代码示例说明了如何实现自己的类加载器,以及类加载器的使用场景。

最后说明了在工作中常遇到的两个与类加载器相关的异常。

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3190 引用 • 8214 回帖 • 1 关注
  • SSL

    SSL(Secure Sockets Layer 安全套接层),及其继任者传输层安全(Transport Layer Security,TLS)是为网络通信提供安全及数据完整性的一种安全协议。TLS 与 SSL 在传输层对网络连接进行加密。

    70 引用 • 193 回帖 • 418 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...