1. 类加载器子系统概述
类加载器子系统的主要功能是 加载、链接和初始化类,并为每个类生成唯一的运行时表示。类加载器的工作是在程序运行时动态加载字节码文件(.class 文件),将其转化为 Java 虚拟机可以理解的格式,并交给 JVM 执行。
类加载器子系统通常遵循以下流程:
- 加载:查找并加载
.class文件。 - 验证:确保字节码的正确性和安全性。
- 准备:为类的静态字段分配内存并赋默认值。
- 解析:将符号引用转换为直接引用。
- 初始化:为类的静态字段赋值,并执行类的静态初始化代码(即
static代码块)。
2 类加载器的类型
Java 虚拟机中的类加载器体系是分层次的,通常包括三种主要的类加载器:
2.1 启动类加载器(Bootstrap ClassLoader)
- 作用:启动类加载器是 JVM 内置的类加载器,它负责加载核心类库,通常是
rt.jar等 JDK 提供的核心 Java 类,例如java.lang、java.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这样的核心类必须由启动类加载器加载,确保不会被用户自定义的类替换或篡改。
加载流程:
- 当应用程序类加载器要加载一个类时,它会先将请求委托给扩展类加载器。
- 扩展类加载器再将请求委托给启动类加载器。
- 如果启动类加载器找不到该类,则会回到扩展类加载器,扩展类加载器也找不到的话,才会由应用程序类加载器来加载。
3.2 示例
比如,当应用程序试图加载 java.lang.String 类时,应用程序类加载器将此请求委托给扩展类加载器,扩展类加载器再委托给启动类加载器。由于 java.lang.String 是核心类库的一部分,启动类加载器会直接加载它。
4. 类加载的生命周期
类加载的整个生命周期可以分为以下几个阶段:
4.1 加载
- 查找类的二进制字节码:类加载器会通过文件路径、网络路径等查找相应的
.class文件或 JAR 文件中的字节码。 - 加载类的字节码:类加载器通过输入流将
.class文件加载到内存中。 - 创建
Class对象:JVM 会根据字节码为类生成一个Class对象,作为该类的元数据。
4.2 验证
- 验证加载的字节码文件是否合法,确保它符合 JVM 的字节码规范,并且没有破坏虚拟机安全性。
- 验证包括四个部分:文件格式验证、元数据验证、字节码验证和符号引用验证。
4.3 准备
- 为类的静态字段分配内存,并初始化为默认值(如
int初始化为0,boolean初始化为false等)。 - 注意,此时并不会赋值为我们在代码中定义的具体值,而是系统默认值。
4.4 解析
- 将类、字段、方法的符号引用(如类名、方法名等)解析为实际的内存地址,即将符号引用转换为直接引用。
- 符号引用是一种间接引用,在字节码中表示为文本符号,而直接引用是内存地址或指针,指向实际的类或方法位置。
4.5 初始化
- 在该阶段,JVM 会初始化静态变量,并执行类中的静态代码块。
- 这也是类的加载过程中唯一会执行代码的阶段。初始化之前的所有阶段都只是进行类的元数据处理,并不会真正执行应用代码。
5. 类加载器的自定义
在 Java 中,开发者可以通过继承 ClassLoader 来自定义类加载器。自定义类加载器的主要用途包括:
- 加载自定义路径下的类:如从网络、数据库、加密文件中加载类。
- 模块化应用:为大型应用程序设计模块化的类加载策略,确保类在模块之间的隔离。
- 替代类加载:为了特定需求,覆盖某些类加载行为。
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 使用自己的 DexClassLoader 和 PathClassLoader 来加载 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 应用程序时的工作流程如下:
- 应用编译:Java 源代码编译成标准的 JVM 字节码(.class 文件)。
- 转换为 DEX 文件:通过 Android 的编译工具将 .class 文件转换为 Dalvik 的 .dex 格式。
- 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 VM | ART (Android Runtime) |
|---|---|---|
| 编译方式 | JIT 编译(运行时编译) | AOT 编译(安装时编译) |
| 字节码格式 | DEX 文件 | DEX 文件 |
| 垃圾回收 | 标记-清除算法,带暂停时间(GC Pause) | 并发、增量垃圾回收,暂停时间减少 |
| 启动时间 | 启动时间较长 | 启动时间更快 |
| 运行时性能 | 依赖 JIT,性能较 Dalvik 低 | 预编译为机器码,性能更高 |
| 内存占用 | 内存占用较大 | 内存占用优化,适合现代设备 |
| 调试和诊断 | 功能较弱 | 提供详细的调试和性能分析工具 |
6.2 Android类加载器
6.3 Android热修复的实现
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();
}
}