likes
comments
collection
share

一文搞懂面试官老问的 Java 类加载机制

作者站长头像
站长
· 阅读数 12

一、介绍

Java 类加载机制的作用和重要性

Java 类加载机制是 Java 运行时的核心组成部分,负责在程序运行过程中动态加载和连接类文件,并将其转换为可执行代码。

  1. 实现动态加载:Java 类加载机制允许程序在运行时根据需要动态地加载类文件。这种能力使得开发人员可以根据实际需求来加载所需的类,而不需要一次性加载所有的类。这对于大型应用程序和框架来说特别有用,因为它们可能包含大量的类,但只有在需要时才会加载。

  2. 解决依赖关系:Java 类加载机制可以解决类之间的依赖关系。当一个类引用另一个类时,如果被引用的类还未被加载,类加载机制会自动触发对被引用类的加载。这种机制确保了类的正确顺序加载,避免了由于依赖关系引起的编译错误或运行时错误。

  3. 实现类的隔离性:Java 类加载器通过不同的命名空间实现了类的隔离性。每个类加载器都有自己的命名空间,同一个类可以被不同的类加载器加载多次,每次都会生成独立的类对象。这种隔离性使得不同的模块或应用可以使用不同的类版本,避免了类之间的冲突。

  4. 支持动态代理和反射:Java 类加载机制为动态代理和反射提供了基础。通过类加载器,我们可以在运行时动态地生成代理类,并且可以在运行时获取和操作类的字段、方法等信息。这种能力使得 Java 在面向对象编程和框架设计方面更加灵活和强大。

  5. 实现安全性和权限控制:Java 类加载机制可以实现安全性和权限控制。它可以限制某些类只能由特定的类加载器加载,从而控制类的访问权限。这对于保护系统的安全性和防止恶意代码的执行至关重要。

二、类加载器

加载器类别

启动类加载器(Bootstrap Class Loader)

启动类加载器(Bootstrap Class Loader)是 Java 类加载器中的最顶层的一个加载器,它负责加载 Java 运行时核心类库和其他被 JVM 所信任的重要类。

  1. 加载核心类库:启动类加载器负责加载 Java 运行时所需的核心类库,包括Java标准库(rt.jar)和扩展库(ext目录下的jar文件)。这些类库包含了 Java 语言的基本类,如Object、String等,以及Java运行时的核心类,如Class、ClassLoader等。

  2. 无法直接获取:启动类加载器是 JVM 实现的一部分,通常由系统或者虚拟机实现语言编写,无法在Java代码中直接获取到。它是 JVM 的一部分,存在于JVM内核中。

  3. 实现为本地代码:由于启动类加载器是 JVM 实现的一部分,因此它的实现通常是用本地代码(C/C++)编写的。这样可以保证加载器的安全性和效率。

  4. 非Java类加载器:启动类加载器不是一个普通的Java类加载器,它不继承自java.lang.ClassLoader类(Java中所有类加载器的基类),而是由JVM实现为特殊的逻辑加载器。

  5. 独立于Java应用:启动类加载器是在 JVM 启动过程中被创建的,它独立于任何Java应用程序。它的主要目的是加载核心类库,为后续的类加载器提供基础。

  6. 位于引导类路径上:启动类加载器从一个特定的位置加载类,该位置被称为引导类路径(Bootstrap Classpath)。引导类路径通常是 JVM 实现的一部分,并且会根据不同的 JVM 实现而有所不同。

  7. 无法自定义:由于启动类加载器是 JVM 的一部分,因此无法对其进行自定义或替换。它负责加载 Java 运行时的基础类库,保证了Java程序运行的稳定性和安全性。

扩展类加载器(Extension Class Loader)

扩展类加载器(Extension Class Loader)是 Java 类加载器中的一种,它是属于标准的系统类加载器的一部分。扩展类加载器用于加载 Java 虚拟机扩展目录(Java Extension Directory)中的类库。

  1. 加载扩展目录:扩展类加载器负责加载 Java 虚拟机的扩展目录(Java Extension Directory),该目录的位置由系统属性 java.ext.dirs 指定。默认情况下,扩展目录位于 <JRE_HOME>/lib/ext 目录下。

  2. 扩展目录的作用:扩展目录是用于存放 Java 虚拟机的扩展类库或第三方库的目录。它提供了一种在 Java 平台上安装额外功能的机制,使得开发人员可以通过简单地将 JAR 文件放置到扩展目录中来扩展 Java 的功能。

  3. 从父类加载器继承:扩展类加载器是标准的系统类加载器的一个实例,它从父类加载器(一般是启动类加载器)继承加载类的能力。这意味着当扩展类加载器无法加载一个类时,它会委派给父类加载器来尝试加载。

  4. 加载扩展类库:扩展类加载器主要用于加载扩展目录中的类库。扩展目录中的类库是一些提供额外功能或扩展 Java API 的类库,它们通常以 JAR 文件的形式存在。

  5. 独立于应用程序:扩展类加载器是独立于应用程序的,它是 JVM 内置的一个类加载器,负责加载系统级别的类库。它和应用程序的类加载器(如应用类加载器)是相互独立的。

  6. 可以自定义扩展目录:通过修改系统属性 java.ext.dirs,我们可以自定义扩展目录的位置。这样,我们可以将一些额外的类库放置到自定义的扩展目录中,并由扩展类加载器加载。

  7. 实现为纯Java类:扩展类加载器的实现是一个普通的 Java 类,它继承自 java.net.URLClassLoader。这使得我们可以通过 Java 代码来获取扩展类加载器实例,并与其交互。

应用程序类加载器(Application Class Loader)

应用程序类加载器(Application Class Loader),也称为系统类加载器(System Class Loader),是 Java 类加载器中的一种。它负责加载应用程序的类和资源文件。

  1. 加载应用程序类:应用程序类加载器是负责加载应用程序的类的主要类加载器。它从指定的类路径(Classpath)中加载类文件,包括应用程序的源代码编译后生成的字节码文件。

  2. 类路径的设置:类路径是指用于查找类文件的路径列表。在运行 Java 程序时,我们可以通过命令行参数 -classpath 或简写的 -cp 来设置类路径。类路径可以包含目录和 JAR 文件。

  3. 与系统类加载器关联:应用程序类加载器是系统类加载器的实例,它继承了系统类加载器的行为和能力。系统类加载器是ClassLoader类的子类,Java虚拟机在启动时自动创建了一个系统类加载器,并将其指定给应用程序类加载器。

  4. 搜索顺序:应用程序类加载器按照特定的搜索顺序加载类。首先,它会尝试使用自己的类路径加载类文件。如果找不到,则会委派给父类加载器,依次往上搜索,直到达到顶层的启动类加载器为止。这种委派模型称为双亲委派模型。

  5. 加载应用程序资源:除了加载类文件,应用程序类加载器还负责加载应用程序的资源文件。资源文件可以是文本文件、配置文件、图片等应用程序所需的其他非类文件。通过应用程序类加载器,我们可以使用 getResource()getResourceAsStream() 方法来获取应用程序的资源。

  6. 可以自定义类加载器:虽然应用程序类加载器是由Java虚拟机在运行时自动创建的,但我们也可以通过编写自定义的类加载器来替换或扩展它的功能。自定义类加载器可以用于实现特定的类加载策略,如从数据库、网络或非标准路径加载类。

  7. 独立于应用程序:应用程序类加载器是与特定的应用程序相关联的,它负责加载应用程序的类和资源。每个应用程序都有自己独立的应用程序类加载器,不同的应用程序之间相互独立。

双亲委派模型(Delegation Model)

双亲委派模型(Parent Delegation Model)是 Java 类加载器的一种工作机制,它定义了类加载器之间的层次关系和类加载的优先级。

  1. 类加载器层次结构:在 Java 类加载器中,存在一个层次结构,由多个类加载器按照特定的顺序组成。这个层次结构通常被称为类加载器链。

  2. 父子关系:双亲委派模型中,每个类加载器都有一个父类加载器(除了根类加载器),它们之间通过组合关系建立起层次结构。一个类加载器的父加载器通常是其上一级的加载器。

  3. 加载优先级:当一个类加载器需要加载一个类时,它首先将加载请求委派给其父类加载器。如果父加载器能够加载该类,那么就直接返回该类;否则,才由子加载器尝试加载。这样一层一层的委派下去,直到父加载器无法加载或者到达最顶层的启动类加载器。

  4. 避免重复加载:双亲委派模型的核心思想是避免重复加载类。在加载过程中,如果某个类已经由父类加载器加载过了,那么子类加载器就不再加载,直接使用父加载器已加载的版本。这样能够确保同一个类在内存中只有一份,避免了类的重复定义和冲突。

  5. 安全性保证:双亲委派模型也提供了一定的安全性保证。通过设置不同的类加载器层次结构,可以控制类加载的权限。核心的 Java API 类库通常由启动类加载器加载,而应用程序自定义的类则由应用程序类加载器加载。这样,核心类库的类无法被重新定义或篡改,保障了Java平台的稳定和安全。

  6. 自定义类加载器:双亲委派模型也为自定义类加载器提供了基础。通过继承 ClassLoader 类并重写其方法,我们可以自定义类加载器,实现特定的类加载策略。自定义类加载器可以在加载类的过程中修改默认的委派行为,实现一些特殊需求。

双亲委派模型是 Java 类加载器的一种工作机制,定义了类加载器之间的层次关系和类加载的优先级。它通过委派机制,使得父加载器先尝试加载类,避免了重复加载和类的冲突。双亲委派模型也提供了一定的安全性保证,控制了类加载的权限。同时,双亲委派模型也为自定义类加载器提供了基础,可以实现特定的类加载策略。

自定义类加载器

自定义类加载器是通过继承ClassLoader类来实现的。ClassLoader是Java虚拟机提供的用于加载类和资源的基础类加载器,自定义类加载器可以在其基础上实现特定的加载策略。

  1. 继承ClassLoader类:创建一个自定义类加载器需要定义一个类,并继承ClassLoader类。
public class CustomClassLoader extends ClassLoader {
    // 自定义类加载器的实现
}
  1. 重写findClass()方法:在自定义类加载器中,需要重写findClass(String name)方法。这个方法负责根据类的名称查找并加载类的字节码。
public class CustomClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] byteCode = loadClassBytes(name); // 加载类的字节码
        if (byteCode == null) {
            throw new ClassNotFoundException();
        }
        return defineClass(name, byteCode, 0, byteCode.length);
    }

    private byte[] loadClassBytes(String name) {
        // 根据name加载类的字节码,可从文件系统、网络等位置加载
        // 返回字节码的字节数组
    }
}
  1. 加载类的字节码:在findClass()方法中,我们需要根据类的名称加载相应的字节码。这一步可以根据需求自由实现,例如从文件系统、网络或其他非标准位置加载。
private byte[] loadClassBytes(String name) {
    // 根据name加载类的字节码,可从文件系统、网络等位置加载
    // 返回字节码的字节数组
    try (InputStream inputStream = new FileInputStream(name + ".class")) {
        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[4096];
        int bytesRead;
        while ((bytesRead = inputStream.read(buffer)) != -1) {
            byteStream.write(buffer, 0, bytesRead);
        }
        return byteStream.toByteArray();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return null;
}

这个示例中,我们通过从文件系统加载类的字节码。你可以根据实际需求自行实现,例如从网络下载字节码或从其他自定义位置加载。

  1. 使用自定义类加载器:完成自定义类加载器的实现后,我们可以使用它来加载自己定义的类。
public class Main {
    public static void main(String[] args) {
        CustomClassLoader classLoader = new CustomClassLoader();
        try {
            Class<?> customClass = classLoader.loadClass("com.example.CustomClass");
            // 使用加载的类进行操作
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

在这个示例中,我们创建了一个CustomClassLoader实例,并调用loadClass()方法加载指定的类。返回的Class对象可以用于实例化对象或调用类的静态方法。

  1. 类加载器命名空间:自定义类加载器还可以实现类加载器命名空间的隔离。这样,不同的类加载器加载的类相互隔离,避免类的冲突。
public class CustomClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        if (shouldLoadWithCustomClassLoader(name)) {
            byte[] byteCode = loadClassBytes(name);
            if (byteCode == null) {
                throw new ClassNotFoundException();
            }
            return defineClass(name, byteCode, 0, byteCode.length);
        } else {
            return super.findClass(name);
        }
    }

    private boolean shouldLoadWithCustomClassLoader(String name) {
        // 根据自定义规则判断是否由自定义类加载器加载
        // 返回true表示由自定义类加载器加载,返回false表示由父类加载器加载
    }
}

在这个示例中,我们通过重写findClass()方法,根据自定义规则判断是否应该由自定义类加载器加载类。如果满足条件,则调用defineClass()方法加载类的字节码;否则,则交给父类加载器处理。

三、类加载过程

加载阶段(Loading)

  1. 查找并加载类的字节码文件:Java虚拟机根据类的全限定名在文件系统、网络或其他来源中查找并读取类的字节码文件。字节码文件通常以.class文件形式存在。
  2. 创建对应的Class对象:一旦字节码文件被获取,Java虚拟机会将其转化为一个Class对象。这个Class对象包含了类的结构信息,并用于在运行时动态创建对象。

验证阶段(Verification)

  1. 文件格式验证:Java虚拟机对字节码文件的格式进行验证,确保它符合Java虚拟机规范定义的文件格式要求。
  2. 字节码验证:验证字节码的逻辑合法性,避免潜在的类型安全问题。
  3. 符号引用验证:对类中的符号引用进行验证,确保引用的目标是有效的。
  4. 内部一致性验证:验证类的内部结构是否一致,比如父类与子类之间的继承关系是否正确。

准备阶段(Preparation)

  1. 为静态变量分配内存空间:Java虚拟机为类的静态变量在内存中分配空间,这些变量被存储在方法区中。
  2. 设置默认初始值:静态变量被初始化为默认值,比如整数类型变量初始化为0,引用类型变量初始化为null。

解析阶段(Resolution)

  1. 符号引用转换为直接引用:Java虚拟机将符号引用转换为直接引用,以便后续使用。
  2. 类、接口、字段和方法解析:将符号引用解析为对应的类、接口、字段和方法的直接引用,以便进一步操作。

初始化阶段(Initialization)

  1. 执行静态变量赋值和静态代码块:Java虚拟机执行类的初始化代码,给静态变量赋予初始值,并执行静态代码块中的代码。这些代码通常用于完成静态变量的初始化以及其他一次性的初始化工作。

使用阶段(Usage)

  1. 创建实例对象:在类加载完成后,可以通过构造函数创建类的实例对象。
  2. 调用方法:通过对象调用类的方法。
  3. 访问字段:通过对象访问类的字段(成员变量)。

四、类的卸载

垃圾回收对类的卸载处理

在Java虚拟机中,垃圾回收(Garbage Collection)是负责自动回收不再使用的内存资源的机制。当一个类不再被使用时,Java虚拟机会通过垃圾回收来回收该类所占用的内存空间,并对其进行卸载处理。

垃圾回收器通过标记-清除(Mark and Sweep)算法或其他相关算法来识别和回收无用的对象。当垃圾回收器确定某个类的所有实例对象都已经不再被引用时,它可以判断该类已经不再需要存在于内存中。

在垃圾回收过程中,如果一个类的所有实例对象都被回收,那么虚拟机将执行类的卸载操作。类卸载的具体过程如下:

  1. 首先,虚拟机将检查该类的所有实例对象是否都已经被回收。这可以通过遍历堆内存中的对象来完成。
  2. 如果虚拟机确认该类的所有实例对象都已经被回收,那么它将进一步检查该类的类加载器是否也已经不再被引用。
  3. 如果类加载器也不再被引用,那么虚拟机就可以卸载该类。卸载操作会释放该类所占用的内存空间,并且将该类在方法区中的相关信息清除。

需要注意的是,类的卸载是一个相对较为复杂的操作,且其具体实现可能因Java虚拟机的不同而有所差异。一般情况下,只有当满足特定条件时,垃圾回收器才会触发类的卸载操作,例如类的所有实例对象都已经被回收,并且该类的类加载器也不再被引用。

卸载条件和判定过程

类的卸载是一个相对较为复杂的操作,其触发和判定过程可能因Java虚拟机的不同而有所差异。一般情况下,以下条件之一满足时,垃圾回收器才会触发类的卸载操作:

  1. 该类的所有实例对象都已经被回收。
  2. 该类的类加载器已经不再被引用。

卸载判定过程:

在Java虚拟机中,类的卸载判定通常是由垃圾回收器来完成的。具体的卸载判定过程可能因虚拟机的实现而有所不同,但一般会包括以下步骤:

  1. 根据特定的条件触发垃圾回收:当满足一定的条件时,虚拟机会触发垃圾回收。这些条件可以是虚拟机自身设定的策略,或者通过用户设置的参数进行配置。

  2. 标记阶段:垃圾回收器会对堆内存中的对象进行标记,以识别哪些对象是可达的,哪些对象是不可达的。可达对象通常意味着它们仍然被引用,不可达对象则表示它们已经没有与之关联的引用。

  3. 清除阶段:在标记阶段之后,垃圾回收器会清除那些被标记为不可达的对象。这样一来,堆内存中就会释放出那些不再被引用的对象所占用的空间。

  4. 类的卸载判定:在清除阶段之后,垃圾回收器会进一步检查类的卸载条件。它会遍历已加载的类,并检查每个类的实例对象是否都已经被回收。如果一个类的所有实例对象都已经被回收,那么虚拟机会继续检查该类的类加载器是否还被引用。

  5. 卸载操作:如果一个类的所有实例对象都已经被回收,并且该类的类加载器也不再被引用,那么虚拟机可以执行类的卸载操作。卸载操作将释放该类所占用的内存空间,并且将该类在方法区中的相关信息清除。

需要注意的是,垃圾回收和类的卸载通常是由虚拟机自动进行的,我们不需要显式地触发或管理类的卸载过程。这种自动化的内存管理机制确保了Java程序的运行效率和稳定性。

五、类加载机制的应用场景

  1. 动态类加载和使用:类加载机制允许在运行时动态地加载和使用类。这对于实现插件化、模块化和动态扩展的应用非常有用。例如,Java中的反射机制就依赖于类加载机制,通过加载和使用未知类的信息,实现了动态调用和绑定。

  2. 自定义类加载器:Java提供了自定义类加载器的能力,开发人员可以根据特定需求实现自定义的类加载器,从而加载非标准位置或非标准格式的类文件。这在一些特殊场景下很有用,比如热部署、代码隔离和安全性管理。

  3. 动态代理和AOP(面向切面编程):类加载机制可以通过动态代理生成代理类,实现日志记录、事务管理等横切逻辑的切面编程。通过在类加载过程中对字节码进行增强,可以在运行时动态地为类添加额外的功能。

  4. 类的隔离和沙箱环境:类加载机制对类的加载、访问和使用进行了严格的控制,可以实现类的隔离和沙箱环境。这在安全性要求较高的环境中非常重要,比如浏览器插件、操作系统级别的应用程序和网络服务器。

  5. 类加载器链和模块化:类加载机制通过类加载器链的方式,实现了类的层次化加载和命名空间的划分,促进了模块化开发和组件化架构。比如Java的模块化系统——Java Platform Module System(JPMS),依赖于类加载机制来加载和管理模块。

  6. 运行时类型信息(RTTI):类加载机制提供了运行时类型信息的支持,包括获取类的名称、方法、字段等元数据信息,以及进行类型检查和转换。这对于动态编程和反射等场景非常有用。

转载自:https://juejin.cn/post/7267092779280482345
评论
请登录