Java-基础-05-JVM-2-类加载器子系统

184 阅读11分钟

1. 类加载器子系统概述

类加载器子系统的主要功能是 加载、链接和初始化类,并为每个类生成唯一的运行时表示。类加载器的工作是在程序运行时动态加载字节码文件(.class 文件),将其转化为 Java 虚拟机可以理解的格式,并交给 JVM 执行。

类加载器子系统通常遵循以下流程:

  1. 加载:查找并加载 .class 文件。
  2. 验证:确保字节码的正确性和安全性。
  3. 准备:为类的静态字段分配内存并赋默认值。
  4. 解析:将符号引用转换为直接引用。
  5. 初始化:为类的静态字段赋值,并执行类的静态初始化代码(即 static 代码块)。

2 类加载器的类型

Java 虚拟机中的类加载器体系是分层次的,通常包括三种主要的类加载器:

2.1 启动类加载器(Bootstrap ClassLoader)

  • 作用:启动类加载器是 JVM 内置的类加载器,它负责加载核心类库,通常是 rt.jar 等 JDK 提供的核心 Java 类,例如 java.langjava.util 等基础类。
  • 实现:它不是由 Java 代码实现的,而是由底层的 C/C++ 实现。
  • 加载范围:它加载的是 JDK 自带的核心类库,位于 $JAVA_HOME/jre/lib 目录下的文件。

2.2 扩展类加载器(Extension ClassLoader)

  • 作用:扩展类加载器加载扩展的库,通常是 Java 的一些标准扩展类库。
  • 实现:由 sun.misc.Launcher$ExtClassLoader 实现。
  • 加载范围:它加载的类位于 $JAVA_HOME/jre/lib/ext 目录下,也可以通过系统属性 java.ext.dirs 来指定扩展类库的目录。

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

  • 作用:应用程序类加载器(也叫系统类加载器)负责加载应用程序的类文件,通常是我们编写的应用代码以及外部的第三方库。
  • 实现:由 sun.misc.Launcher$AppClassLoader 实现。
  • 加载范围:它会加载 classpath 路径下的类文件,包括 .class 文件和 JAR 文件。

3. 类加载器的双亲委派模型

3.1 双亲委派机制

双亲委派模型(Parent Delegation Model)是类加载器子系统的核心设计,它规定:

  • 当一个类加载器收到类加载请求时,它不会直接加载类文件,而是先将该请求委托给它的父类加载器。
  • 只有当父类加载器无法加载该类时,当前加载器才会尝试自己加载。

这种设计的主要目的是:

  • 防止重复加载:避免子加载器加载已经被父加载器加载过的类。
  • 保证核心类的安全性:如 java.lang.String 这样的核心类必须由启动类加载器加载,确保不会被用户自定义的类替换或篡改。

加载流程

  1. 当应用程序类加载器要加载一个类时,它会先将请求委托给扩展类加载器。
  2. 扩展类加载器再将请求委托给启动类加载器。
  3. 如果启动类加载器找不到该类,则会回到扩展类加载器,扩展类加载器也找不到的话,才会由应用程序类加载器来加载。

3.2 示例

比如,当应用程序试图加载 java.lang.String 类时,应用程序类加载器将此请求委托给扩展类加载器,扩展类加载器再委托给启动类加载器。由于 java.lang.String 是核心类库的一部分,启动类加载器会直接加载它。

image.png

4. 类加载的生命周期

类加载的整个生命周期可以分为以下几个阶段:

4.1 加载

  • 查找类的二进制字节码:类加载器会通过文件路径、网络路径等查找相应的 .class 文件或 JAR 文件中的字节码。
  • 加载类的字节码:类加载器通过输入流将 .class 文件加载到内存中。
  • 创建 Class 对象:JVM 会根据字节码为类生成一个 Class 对象,作为该类的元数据。

4.2 验证

  • 验证加载的字节码文件是否合法,确保它符合 JVM 的字节码规范,并且没有破坏虚拟机安全性。
  • 验证包括四个部分:文件格式验证、元数据验证、字节码验证和符号引用验证。

4.3 准备

  • 为类的静态字段分配内存,并初始化为默认值(如 int 初始化为 0boolean 初始化为 false 等)。
  • 注意,此时并不会赋值为我们在代码中定义的具体值,而是系统默认值。

4.4 解析

  • 将类、字段、方法的符号引用(如类名、方法名等)解析为实际的内存地址,即将符号引用转换为直接引用。
  • 符号引用是一种间接引用,在字节码中表示为文本符号,而直接引用是内存地址或指针,指向实际的类或方法位置。

4.5 初始化

  • 在该阶段,JVM 会初始化静态变量,并执行类中的静态代码块。
  • 这也是类的加载过程中唯一会执行代码的阶段。初始化之前的所有阶段都只是进行类的元数据处理,并不会真正执行应用代码。

5. 类加载器的自定义

在 Java 中,开发者可以通过继承 ClassLoader 来自定义类加载器。自定义类加载器的主要用途包括:

  1. 加载自定义路径下的类:如从网络、数据库、加密文件中加载类。
  2. 模块化应用:为大型应用程序设计模块化的类加载策略,确保类在模块之间的隔离。
  3. 替代类加载:为了特定需求,覆盖某些类加载行为。

5.1 自定义类加载器示例

java
复制代码
public class MyClassLoader extends ClassLoader {
    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
        // 定义自定义类加载逻辑,比如从文件或网络中加载类的字节码
        byte[] bytes = loadClassFromCustomSource(name);
        return defineClass(name, bytes, 0, bytes.length);
    }

    private byte[] loadClassFromCustomSource(String name) {
        // 实现加载类字节码的逻辑(可以是从文件、网络等)
        // 例如:读取 .class 文件的字节数据
        return new byte[0];
    }
}

在此示例中,自定义的 MyClassLoader 重写了 findClass 方法,并通过 defineClass 将字节数组转换为 Java 类。

5.2 破坏双亲委派模型

在某些情况下,自定义类加载器可能需要破坏双亲委派模型,这种情况下可以通过覆盖 loadClass 方法实现:

java
复制代码
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // 自己加载类,跳过父类加载器
    return findClass(name);
}

这样就不会再将类加载请求委托给父类加载器,而是直接由当前加载器加载。

6. 类加载器在 Android 中的应用

在 Android 中,类加载器的工作方式与 JVM 中类似,但 Android 使用自己的 DexClassLoaderPathClassLoader 来加载 APK 文件中的 DEX 字节码。主要区别包括:

  • DexClassLoader:用于从 .apk 文件、.dex 文件或 JAR 包中加载类,支持动态加载。
  • PathClassLoader:主要用于加载已经安装在系统中的应用程序类,路径指向 APK 或 DEX 文件。

6.1 Android虚拟机

6.1.1 Dalvik 虚拟机详解

6.1.1.1 架构

Dalvik 虚拟机设计上与传统 JVM 存在较大区别。以下是 Dalvik 虚拟机的几个关键特性:

  • 多应用进程模型:Dalvik 虚拟机为每个应用程序提供了独立的进程和虚拟机实例,这使得 Android 的每个应用都是隔离运行的,保证了应用间的安全性。
  • Register-Based(寄存器)架构:与传统 JVM 使用基于栈的架构不同,Dalvik 虚拟机使用寄存器架构。这种设计在移动设备中更加高效,因为寄存器架构的指令通常比栈指令少,减少了方法调用时的开销。
  • 字节码格式:Dalvik 虚拟机使用 .dex 文件(Dalvik Executable),这是将多个 .class 文件打包成一个较小的文件,以减少重复信息并节省内存空间。DEX 文件是更紧凑的字节码格式,优化了 Android 设备的存储需求。
6.1.1.1.2 工作原理

Dalvik 虚拟机在执行 Android 应用程序时的工作流程如下:

  1. 应用编译:Java 源代码编译成标准的 JVM 字节码(.class 文件)。
  2. 转换为 DEX 文件:通过 Android 的编译工具将 .class 文件转换为 Dalvik 的 .dex 格式。
  3. JIT 编译:在应用运行时,Dalvik 虚拟机通过 JIT 将热点字节码编译为本地机器码,以提升运行时性能。
6.1.1.1.3 垃圾回收机制

Dalvik 虚拟机的垃圾回收机制相对简单,采用标记-清除(Mark-and-Sweep)算法。Dalvik 的 GC 机制会引发“应用暂停”现象(GC Pause Time),即在进行垃圾回收时,整个应用会暂停,等待 GC 结束。这种现象在 Dalvik 上较为明显,影响用户体验。

6.1.2. ART(Android Runtime)详解

ART 虚拟机是 Android 虚拟机的现代实现,解决了 Dalvik 虚拟机中的一些性能问题。以下是 ART 的核心特性:

6.2 AOT 编译

  • 应用安装时编译:ART 的 AOT 编译机制在应用安装时将字节码预编译为本地机器码。相比 Dalvik 的 JIT 编译,这大大减少了应用运行时的编译开销。
  • 编译文件存储:编译后的机器码存储在 /data/app/ 目录下的 .oat 文件中,这些文件是优化后的应用程序代码,确保应用启动时可以直接运行机器码,而无需再进行字节码解释。

6.2.1 改进的垃圾回收(GC)

ART 虚拟机在垃圾回收方面进行了重大改进,主要体现在以下几点:

  • 并发 GC:ART 支持并发垃圾回收(Concurrent GC),垃圾回收的同时,应用程序可以继续运行,减少了 GC 暂停时间。
  • 增量 GC:ART 的增量垃圾回收机制使得垃圾回收过程被分成多个小任务,避免了应用长时间的暂停。
  • 低延迟 GC:ART 对实时性要求较高的场景进行了优化,进一步减少了垃圾回收的延迟。

6.2.2 调试和诊断支持

ART 提供了更多的调试和诊断工具,以便开发者更好地分析和优化应用程序性能:

  • 详细的堆分析:ART 提供更详细的堆信息和垃圾回收日志,便于开发者分析内存分配和回收情况。
  • Profile-Guided Compilation:ART 支持基于应用实际使用情况的优化编译。通过运行时生成的性能分析数据,ART 能够为应用程序提供更有针对性的编译优化。

6.2.3 运行时性能改进

由于 ART 预先编译了应用程序的字节码,应用启动速度和执行效率得到了显著提升。虽然 AOT 编译会导致应用安装时间变长,但通过减少运行时的编译工作,ART 提供了更流畅的用户体验。

6.3. Dalvik 与 ART 的对比

特性Dalvik VMART (Android Runtime)
编译方式JIT 编译(运行时编译)AOT 编译(安装时编译)
字节码格式DEX 文件DEX 文件
垃圾回收标记-清除算法,带暂停时间(GC Pause)并发、增量垃圾回收,暂停时间减少
启动时间启动时间较长启动时间更快
运行时性能依赖 JIT,性能较 Dalvik 低预编译为机器码,性能更高
内存占用内存占用较大内存占用优化,适合现代设备
调试和诊断功能较弱提供详细的调试和性能分析工具

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

6.2 Android类加载器

image.png

image.png

6.3 Android热修复的实现

image.png

6.3.1. 准备修复的代码

编译修复的 Java 代码,生成一个新的 Dex 文件(例如,fix.dex)。

6.3.2. 创建自定义类加载器

扩展 ClassLoader 类,创建一个自定义的类加载器用于加载新的 Dex 文件。

java
复制代码
public class RepairClassLoader extends ClassLoader {
    private String dexPath;

    public RepairClassLoader(String dexPath, ClassLoader parent) {
        super(parent);
        this.dexPath = dexPath;
    }

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

    private byte[] loadClassData(String className) {
        // 实现读取 dex 文件并返回字节数组
        // 这里可以使用 DexClassLoader 或其他方式来读取 Dex 文件
        return null; // 返回相应的字节数组
    }
}
6.3.3. 加载新的 Dex 文件

在合适的时机(例如应用启动或通过某种触发机制)加载新的 Dex 文件。

java
复制代码
public void loadFixDex(String fixDexPath) {
    RepairClassLoader classLoader = new RepairClassLoader(fixDexPath, getClassLoader());
    try {
        Class<?> clazz = classLoader.loadClass("com.example.FixClass");
        // 可以通过反射调用修复类中的方法
        Method method = clazz.getMethod("fixMethod");
        method.invoke(null); // 调用修复方法
    } catch (Exception e) {
        e.printStackTrace();
    }
}
6.3.4. 修复已存在的类

通过反射机制将新加载的类的静态方法或字段替换掉原有类的实现。

示例:使用 DexClassLoader

你可以使用 Android 提供的 DexClassLoader 来加载修复的 Dex 文件,这样可以更方便地管理和加载:

java
复制代码
private void loadFixDex(String fixDexPath) {
    DexClassLoader classLoader = new DexClassLoader(fixDexPath,
            getCacheDir().getAbsolutePath(),
            null,
            getClassLoader());

    try {
        Class<?> clazz = classLoader.loadClass("com.example.FixClass");
        Method method = clazz.getDeclaredMethod("fixMethod");
        method.invoke(null);
    } catch (Exception e) {
        e.printStackTrace();
    }
}