JVM-类加载器分析

102 阅读5分钟

知其然要知其所以然,探索每一个知识点背后的意义,你知道的越多,你不知道的越多,一起学习,一起进步,如果文章感觉对您有用的话,关注、收藏、点赞,有困惑的地方可以评论,我们一起探讨!

1. 类加载器的作用

类加载器的主要作用是将类的字节码文件(.class 文件)从文件系统、网络或其他来源加载到 JVM 中,并将其转换为 java.lang.Class 对象,以便 JVM 可以使用这些类。类加载器还负责确保类的唯一性,即同一个类在 JVM 中只会被加载一次。

2. 类加载的过程

类加载的过程主要分为三个阶段:加载、链接和初始化。

  • 加载:查找并加载类的字节码文件。类加载器根据类的全限定名,通过各种方式(如文件系统、网络等)找到对应的字节码文件,并将其读取到内存中。

  • 链接:链接阶段又分为验证、准备和解析三个步骤。

    • 验证:确保加载的字节码文件符合 JVM 的规范,不会对 JVM 的安全造成威胁。
    • 准备:为类的静态变量分配内存,并设置默认初始值。
    • 解析:将类中的符号引用转换为直接引用。
  • 初始化:执行类的初始化代码,包括静态变量的赋值和静态代码块的执行。

3. 类加载器的层次结构

JVM 中的类加载器采用双亲委派模型,形成了一个层次结构,主要包括以下几种类加载器:

  • 启动类加载器(Bootstrap Class Loader) :它是最顶层的类加载器,由 JVM 自身实现,负责加载 JVM 核心类库,如 java.langjava.util 等。启动类加载器没有父类加载器,它加载的类位于 JDK 的 lib 目录下,或者通过 -Xbootclasspath 参数指定的路径。
  • 扩展类加载器(Extension Class Loader) :它是启动类加载器的子类,由 sun.misc.Launcher$ExtClassLoader 实现,负责加载 JDK 的扩展类库,位于 JDK 的 lib/ext 目录下,或者通过 java.ext.dirs 系统属性指定的路径。
  • 应用类加载器(Application Class Loader) :它是扩展类加载器的子类,由 sun.misc.Launcher$AppClassLoader 实现,负责加载用户类路径(classpath)下的类。通常,我们自己编写的 Java 类都是由应用类加载器加载的。
  • 自定义类加载器(Custom Class Loader) :用户可以根据需要自定义类加载器,继承自 java.lang.ClassLoader 类,用于实现特定的类加载逻辑,如从网络、数据库等加载类。

image.png

4. 双亲委派模型

双亲委派模型是 JVM 类加载器的工作机制,其核心思想是:当一个类加载器收到类加载请求时,它首先不会自己去尝试加载这个类,而是将请求委派给父类加载器去完成,每一层的类加载器都是如此,直到到达启动类加载器。只有当父类加载器无法加载该类时,子加载器才会尝试自己加载。

双亲委派模型的源码:

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 {
                    // 父类加载器为 null,说明是启动类加载器,尝试由启动类加载器加载
                    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;
    }
}

5. 双亲委派模型的优点

  • 安全性:通过双亲委派模型,核心类库由启动类加载器加载,避免了用户自定义的类替换核心类库的问题,保证了 JVM 的安全性。
  • 避免重复加载:每个类只会被加载一次,避免了类的重复加载,提高了内存利用率。
  • 类的唯一性:确保了同一个类在 JVM 中只有一个 Class 对象,保证了类的唯一性。

6. 打破双亲委派模型

虽然双亲委派模型有很多优点,但在某些情况下,我们可能需要打破双亲委派模型,例如:

  • 热部署:在开发过程中,需要动态加载和替换类,而双亲委派模型会导致类只能被加载一次,无法实现热部署。我们可以用代码热替换(HotSwap)、模块热部署(Hot Deployment)、arthas等等,Spring 官方推荐的热加载方案 —— Spring boot devtools。RestartClassLoader 为自定义的类加载器,其核心是 loadClass 的加载方式。Spring boot devtools 中修改了双亲委托机制,默认优先从自己加载,如果自己没有加载到,则从 parent 进行加载。 这样保证了业务代码可以优先被 RestartClassLoader 加载,进而通过重新加载 RestartClassLoader 完成应用代码部分的重新加载

  • 模块化开发:在双亲委派中,子类加载器可以使用父类加载器已经加载过的类,但是父类加载器无法使用子类加载器加载过的类(类似继承的关系)。
    Java 提供了很多服务提供者接口(SPI,Service Provider Interface),它可以允许第三方为这些接口提供实现,比如数据库中的 SPI 服务 - JDBC。这些 SPI 的接口由 Java 核心类提供,实现者确是第三方。如果继续沿用双亲委派,就会存在问题,提供者由 Bootstrap ClassLoader 加载,而实现者是由第三方自定义类加载器加载。这个时候,顶层类加载就无法使用子类加载器加载过的类。
    要解决上述问题,就需要打破双亲委派原则。

要打破双亲委派模型,需要自定义类加载器,并重写 loadClass 方法,绕过双亲委派的逻辑。以下是一个简单的自定义类加载器示例:

import java.io.*;

public class CustomClassLoader extends ClassLoader {

    private String classPath;

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

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] classData = getClassData(name);
            if (classData == null) {
                throw new ClassNotFoundException();
            } else {
                return defineClass(name, classData, 0, classData.length);
            }
        } catch (IOException e) {
            throw new ClassNotFoundException();
        }
    }

    private byte[] getClassData(String className) throws IOException {
        String path = classPath + File.separatorChar +
                className.replace('.', File.separatorChar) + ".class";
        try (InputStream ins = new FileInputStream(path);
             ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            byte[] buffer = new byte[1024];
            int bytesNumRead;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        }
    }
}