码恋 码恋

ALL YOUR SMILES, ALL MY LIFE.

目录
JVM 系列之JVM类加载机制
/  

JVM 系列之JVM类加载机制

一、引言

Java 程序代码经过编译后会产生 Class 文件,而这些 Class 文件最终需要加载到虚拟机中才能够被运行和使用。而虚拟机是如何加载这些文件的,Class 文件在进入到虚拟机后又会产生什么变化,这些都是在本篇讲述的内容。

二、类加载机制的概念

Java 虚拟机把描述类的数据从 Class 文件加载到内存中,并对数据进行验证、转换解析和初始化,最终形成虚拟机可以直接使用的 Java 类型的过程就是类加载机制。

类加载过程如下图:

16821595061png

  • 加载
    在加载过程中,虚拟机通过类的全限定名查找 Class 文件的字节流,将字节流代表的静态数据结构转化为方法区的运行时数据结构,并生成 Class 对象。
  • 验证
    这一阶段的目的是确保 Class 文件中数据的正确性,保证这些数据被当做代码运行时不会危害虚拟机自身的安全。
  • 准备
    在准备阶段主要是为了给类的静态变量分配内存空间,并设置类变量的初始值。
  • 解析
    解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
  • 初始化
    对类的静态变量,静态代码块执行初始化操作。

三、类加载器

类加载器的作用是根据一个类的全限定名查找 .class 文件,读取它的二进制流,并把创建 java.lang.Class 文件加载到虚拟机的内存中。

类的唯一性是由类的本身和它的类加载器决定的,也就是说两个类来源于同一个 Class 文件,并且被同一个类加载器加载,这两个类才相等。

虚拟机提供了3种类加载器,引导(Bootstrap)类加载器、扩展(Extension)类加载器、系统(System)类加载器(也称应用类加载器)。

  • 引导类类加载器(Bootstrap Classloader)
    引导类加载器主要加载 JVM 自身需要的类,它使用 C++ 语言实现,是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib 路径下的核心类库或 -Xbootclasspath 参数指定的路径下的 jar 包加载到内存中,由于虚拟机是按照文件名识别并加载 jar 包的,如果文件名不被虚拟机识别,即使把 jar 包丢到 lib 目录下也是没有作用的,出于安全考虑,Bootstrap 启动类加载器只加载包名为 java、javax、sun 等开头的类。
  • 扩展类加载器(Extension Classloader)
    扩展类加载器是指 Sun 公司实现的 sun.misc.Launcher$ExtClassLoader 类,由 Java 语言实现的,是 Launcher 的静态内部类,它负责加载 <JAVA_HOME>/lib/ext 目录下或者由系统变量 -Djava.ext.dir 指定位路径中的类库,开发者可以直接使用标准扩展类加载器。
  • 系统类加载器(System Classloader)
    系统类加载器也称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath-D java.class.path 指定路径下的类库,也就是我们经常用到的 classpath 路径,开发者可以直接使用系统类加载器,一般情况下该类加载器是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader() 方法可以获取到该类加载器。

除此之外,开发者还可以自己定义类加载器。Java 虚拟机对 class 文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的 class 文件加载到内存生成 Class 对象,而且加载某个类的 class 文件时,Java 虚拟机采用的是双亲委派模式即把请求交由父类处理。

四、双亲委派模型

系统中的 ClassLoder 在协同工作的时候会默认使用 双亲委派模型 。即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求委派该父类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。当父类加载器无法处理时,才调用自身的 findClass() 方法加载。当父类加载器为 null 时,会使用启动类加载器 BootstrapClassLoader 作为父类加载器。

简单来说,双亲委派模型通过一种层级关系来保证类的安全性和避免类冲突。根据这个模型,当一个类需要被加载时,Java 虚拟机会按照一定的顺序依次向上委派给父类加载器进行加载,直到最顶层的启动类加载器,如果顶层的启动类加载器无法加载该类,再逐级向下委派给子类加载器进行加载。

parents.png

private final ClassLoader parent; 
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) {//父加载器不为空,调用父加载器loadClass()方法处理
                        c = parent.loadClass(name, false);
                    } else {//父加载器为空,使用启动类加载器 BootstrapClassLoader 加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                   //抛出异常说明父类加载器无法完成加载请求
                }

                if (c == null) {
                    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;
        }
    }

双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载( JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类。

五、破坏双亲委派模型

然而,这个双亲委派模型并不是绝对安全的,有时候它可能会被破坏。以下是一些可能破坏双亲委派模型的情况:

1. 自定义类加载器

Java 允许开发者自定义类加载器,这些自定义类加载器可以通过继承java.lang.ClassLoader类来实现。如果开发者在自定义类加载器中重写了类加载的逻辑,可能会绕过双亲委派模型,直接加载指定的类,从而破坏了原本的类加载层次结构。

public class CustomClassLoader extends ClassLoader {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        if (name.startsWith("com.aysaml")) {
            // 在自定义类加载器中直接加载 com.aysaml 包下的类
            return findClass(name);
        }
        return super.loadClass(name);
    }
}

在上面代码中,自定义的CustomClassLoader继承了ClassLoader类,并重写了loadClass方法。当加载的类名以"com.aysaml"开头时,它会绕过双亲委派模型,直接在自定义类加载器中加载该类。

2. 线程上下文类加载器

Java 提供了线程上下文类加载器(Thread Context ClassLoader)的机制,允许线程在运行时动态改变类的加载行为。如果代码中使用了线程上下文类加载器,并且该加载器没有遵循双亲委派模型,就可能破坏了原本的加载顺序。

public class CustomTask implements Runnable {
    @Override
    public void run() {
        // 获取当前线程的上下文类加载器
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        try {
            // 使用线程上下文类加载器加载指定类
            Class<?> clazz = classLoader.loadClass("com.aysaml.MyClass");
            // 执行相应的操作
            // ...
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

上面代码中,CustomTask类实现了Runnable接口,并在run()方法中获取了当前线程的上下文类加载器。然后使用线程上下文类加载器加载了"com.aysaml.MyClass"类,绕过了双亲委派模型。

3. SPI(Service Provider Interface)机制

SPI 是 Java 提供的一种服务扩展机制,允许开发者在运行时加载并实例化某个接口的实现类。SPI机制中使用了java.util.ServiceLoader类来加载实现类,而该类使用了线程上下文类加载器,因此在某些情况下可能会绕过双亲委派模型。
假设我们有一个接口DatabaseDriver和一个实现类MySqlDriver,我们将MySqlDriver的实现类名称放在META-INF/services目录下的com.aysaml.DatabaseDriver文件中。在这种情况下,ServiceLoader类将会使用线程上下文类加载器来加载实现类。

ServiceLoader<DatabaseDriver> serviceLoader = ServiceLoader.load(DatabaseDriver.class);
Iterator<DatabaseDriver> drivers = serviceLoader.iterator();
while (drivers.hasNext()) {
    DatabaseDriver driver = drivers.next();
    // 执行相应的操作
    // ...
}

上面代码中,ServiceLoader使用了线程上下文类加载器来加载DatabaseDriver的实现类,绕过了双亲委派模型。

这些情况下,如果不小心使用或者实现了自定义类加载器、线程上下文类加载器或 SPI 机制,可能会破坏双亲委派模型,导致类加载时的行为与原本预期不符。这可能会引发类冲突、安全性问题或者其他意料之外的行为。因此,在 Java 开发中,应该谨慎使用和实现这些机制,以避免破坏双亲委派模型所带来的问题。

六、模块化类加载器

模块化的类加载器是 Java 9 及其以后版本引入的概念,它与传统的类加载器有所不同。在模块化系统中,应用程序被组织成一系列模块,每个模块都有自己的类路径和依赖关系。模块化类加载器负责加载和管理这些模块。

1. 模块化类加载器的主要特点
  • 模块化命名空间:每个模块都有一个唯一的模块名,模块名用于标识模块并区分其他模块。模块之间的类是相互隔离的,不同模块中的相同类名不会发生冲突。

  • 显式依赖关系:模块之间通过显式声明依赖关系来进行引用。模块可以指定它所依赖的其他模块,这样在加载模块时会自动解析和加载其依赖的模块。

  • 模块路径:与传统的类路径不同,模块路径是指定模块的位置和依赖关系的机制。它由一组模块化的 JAR 文件或目录组成,而不是单个的类路径。

  • 模块化类加载器:模块化类加载器由启动类加载器和应用程序类加载器构成。启动类加载器负责加载 Java 运行时的核心模块,应用程序类加载器负责加载应用程序的模块。每个模块都由自己的类加载器进行加载和管理。

    通过模块化类加载器,Java 应用程序可以更好地管理和隔离模块之间的依赖关系,避免了类冲突和版本冲突的问题。同时,模块化的类加载器提供了更细粒度的控制,允许开发者按需加载和卸载模块,提高了应用程序的灵活性和可维护性。

2. 模块化的类加载器引入的原因
  • 解决类冲突和版本冲突:在传统的类加载机制中,所有类都存在于一个全局的命名空间中,不同的类可能使用相同的类名,导致类冲突。此外,如果多个模块依赖于不同版本的同一个库,也会导致版本冲突。模块化的类加载器通过为每个模块提供独立的命名空间,可以避免这些冲突问题的发生。
  • 提供隔离和安全性:模块化的类加载器将应用程序划分为独立的模块,每个模块都有自己的类路径和依赖关系。这种隔离性确保了模块之间的代码不会相互干扰,也减少了模块之间的耦合性。此外,模块化的类加载器还可以为每个模块设置不同的访问权限,实现更细粒度的安全性控制。
  • 模块化依赖管理:模块化系统引入了显式的依赖关系,模块可以声明其依赖的其他模块。这使得应用程序的依赖关系更加清晰明确,并且在加载模块时可以自动解析和加载其依赖的模块,简化了依赖管理的过程。
  • 灵活的模块加载和卸载:模块化的类加载器允许动态加载和卸载模块。这意味着应用程序可以在运行时根据需要加载所需的模块,或者在不再需要某个模块时卸载它。这提供了更高的灵活性和可扩展性,使得应用程序可以更好地适应变化的需求。
  • 性能优化:模块化的类加载器可以进行更精细的控制和优化,例如只加载所需的模块,减少了不必要的类加载和初始化。这可以提高应用程序的启动速度和内存利用率。
    总而言之,模块化的类加载器通过解决类冲突和版本冲突、提供隔离和安全性、改进依赖管理、支持动态加载和卸载,以及进行性能优化等方面的设计,提供了更强大、灵活和可控的类加载机制。它帮助开发者更好地组织和管理应用程序的模块,提高了应用程序的可维护性、可扩展性和安全性。


❤ 转载请注明本文地址或来源,谢谢合作 ❤


center