类加载器详解(重点)
一、回顾一下类加载过程
类加载的过程可以分为 加载、连接 和 初始化 三个主要阶段。连接过程又包括 验证、准备 和 解析。以下是具体的各个步骤:
1. 加载
类加载的第一步是获取并处理类的二进制字节流,过程包括以下三项:
通过全类名获取定义此类的二进制字节流:
通过类的全限定名(例如com.example.MyClass
),定位并获取包含类字节码的文件。这些字节码文件通常是.class
文件,可能来自本地文件系统、JAR 包、网络等。将字节流所代表的静态存储结构转换为方法区的运行时数据结构:
字节流会被解析为一个包含类信息的内存数据结构,存放在 JVM 的方法区(在 JDK 7 之前是永久代)中。这些信息包括类的字段、方法、常量池等。在内存中生成一个代表该类的
Class
对象,作为方法区这些数据的访问入口:
JVM 创建一个Class
对象,这个对象代表了类的元数据,并可以通过反射 API 访问。Class
对象提供了对类的操作接口。
2. 连接
连接过程包括以下三个阶段:
验证:确保加载的类字节流符合 JVM 的规范,并且不会导致 JVM 安全或稳定性问题。验证通过文件格式、字节码、程序语义等多个方面检查类字节流的正确性。
准备:为类的静态变量分配内存并设置初始默认值。静态变量会被分配在方法区,而非静态变量则会在对象实例化时分配内存。
解析:将常量池中的符号引用(如类、方法、字段的名称)转换为直接引用(即内存地址或偏移量)。这一过程确保 JVM 在执行时能快速找到并访问这些类成员。
3. 初始化
初始化阶段是类加载过程的最后一步,执行类的静态初始化方法 <clinit>()
。在这一阶段,JVM 会为类中的静态变量赋值,并执行类中的静态代码块。
这个阶段通常是由类的首次使用触发的,例如创建类的实例、访问类的静态变量或调用静态方法时。
小结
类加载过程是由 加载、连接(验证、准备、解析)和 初始化 三个主要阶段组成的。每个阶段在类首次被使用时都会按需执行,确保类在 JVM 中正确地加载、验证并初始化,以便后续的使用。
二、类加载器
类加载器介绍
类加载器是 Java 的核心组件之一,它负责将类的字节码(.class 文件)加载到 JVM 中,并生成对应的 Class
对象。最初,类加载器的设计主要是为了支持 Java Applet(现已被淘汰),但随着时间的推移,类加载器成为了 Java 程序中不可或缺的一部分。类加载器不仅可以加载类,还能加载其他资源文件(如文本、配置文件、图像等),但是本文重点讨论其加载类的功能。
类加载器的基本功能
- 类加载器将类的字节码加载到 JVM 中,创建一个
Class
对象,作为对该类的引用。 - 每个 Java 类都有一个
ClassLoader
引用,指向加载该类的类加载器。 - 数组类不由类加载器加载,它们由 JVM 自动生成,且数组的
ClassLoader
与元素类型的ClassLoader
相同。
class Class<T> {
private final ClassLoader classLoader;
@CallerSensitive
public ClassLoader getClassLoader() {
//...
}
}
类加载器的层次结构与规则
JVM 采用懒加载策略,即只有当类真正需要使用时,才会加载该类。每个类加载器都有一个父加载器,通过双亲委派模型来决定加载顺序。
类加载器的三种类型
BootstrapClassLoader
(启动类加载器):- 负责加载 JVM 核心类库,如
rt.jar
中的类。 - 该类加载器由 C++ 实现,因此没有对应的 Java 类,
getClassLoader()
方法会返回null
。
- 负责加载 JVM 核心类库,如
ExtensionClassLoader
(扩展类加载器):- 负责加载
%JRE_HOME%/lib/ext
目录中的类或 JDK 设置的扩展路径中的类。
- 负责加载
AppClassLoader
(应用程序类加载器):- 负责加载用户应用程序的类,通常从应用的 classpath 中加载。
类加载器的父子关系
AppClassLoader
的父加载器是ExtensionClassLoader
,其父加载器是BootstrapClassLoader
。- 通过
getParent()
方法可以访问到类加载器的父加载器。
public abstract class ClassLoader {
private final ClassLoader parent;
@CallerSensitive
public final ClassLoader getParent() {
//...
}
}
类加载器实例
下面是一个打印类加载器层次的示例:
public class PrintClassLoaderTree {
public static void main(String[] args) {
ClassLoader classLoader = PrintClassLoaderTree.class.getClassLoader();
StringBuilder split = new StringBuilder("|--");
boolean needContinue = true;
while (needContinue) {
System.out.println(split.toString() + classLoader);
if (classLoader == null) {
needContinue = false;
} else {
classLoader = classLoader.getParent();
split.insert(0, "\t");
}
}
}
}
输出结果(JDK 8):
|--sun.misc.Launcher$AppClassLoader@18b4aac2
|--sun.misc.Launcher$ExtClassLoader@53bd815b
|--null
从输出可以看出:
PrintClassLoaderTree
使用的是AppClassLoader
加载的。AppClassLoader
的父加载器是ExtClassLoader
,它的父加载器是BootstrapClassLoader
,即null
。
自定义类加载器
除了内置的类加载器,Java 允许用户自定义类加载器,满足特殊的需求。要自定义类加载器,需要继承 ClassLoader
类,并重写 findClass()
或 loadClass()
方法。
重写 findClass()
和 loadClass()
findClass(String name)
:根据类的二进制名称查找类,通常在自定义类加载器中重写此方法来实现类的加载。loadClass(String name, boolean resolve)
:加载类并解析,调用findClass()
方法加载类,若类加载成功,则解析类。
在不打破双亲委派模型的情况下,重写 findClass()
方法即可;如果要打破双亲委派模型,重写 loadClass()
方法。
类加载器总结
- 类加载器 是加载类的核心组件,负责将
.class
文件加载到 JVM 中。 - JVM 中有三个内置的类加载器:
BootstrapClassLoader
、ExtensionClassLoader
和AppClassLoader
。 - 类加载器遵循双亲委派模型,即一个类加载器会委托给它的父类加载器去加载类,直到最顶层的
BootstrapClassLoader
。 - 用户可以自定义类加载器,以满足特殊的需求,例如对类字节码进行加密/解密操作。
三、双亲委派模型
双亲委派模型介绍
双亲委派模型是 Java 类加载机制的重要组成部分,确保了类加载过程中的安全性和一致性。在这个模型中,每个 ClassLoader
都有一个父加载器,当请求加载类时,ClassLoader
会首先将请求委托给父类加载器,如果父加载器不能加载该类,它才会尝试自己加载。这个过程直到顶层的启动类加载器(Bootstrap ClassLoader
)为止。Bootstrap ClassLoader
是虚拟机中的顶层加载器,它不属于任何 ClassLoader
,但是它可以作为其他加载器的父加载器。
该模型的核心思想是通过委派机制,避免了类加载的重复和核心类的篡改。
双亲委派模型的执行流程
双亲委派模型的实现集中在 java.lang.ClassLoader
类的 loadClass()
方法中。执行流程如下:
- 检查是否已经加载过:每当一个类加载器接收到加载请求时,它首先检查该类是否已经加载过。
- 委托父类加载器:如果当前类没有加载,它会将加载请求委托给父类加载器。
- 调用
findClass()
方法:如果父加载器无法加载该类,子加载器才会尝试通过findClass()
方法来加载该类。 - 返回加载的类:如果类加载成功,则返回该类。如果类无法加载,则抛出
ClassNotFoundException
。
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 {
c = findBootstrapClassOrNull(name); // 委托给启动类加载器
}
} catch (ClassNotFoundException e) {
// 父加载器无法加载类,继续尝试
}
if (c == null) {
c = findClass(name); // 调用子类加载器的加载方法
}
}
if (resolve) {
resolveClass(c); // 进行链接操作
}
return c;
}
}
双亲委派模型的好处
- 避免类的重复加载:通过委托父加载器,确保了一个类只被加载一次。
- 核心类的安全性:
Bootstrap ClassLoader
加载 Java 核心类库,保证了核心类的唯一性,防止恶意代码篡改核心类。 - 类加载隔离:不同的类加载器可以加载相同名字的类,避免了类冲突。例如,Web 应用可以使用自己的类加载器加载 Web 特有的类。
打破双亲委派模型的方法
虽然双亲委派模型在大多数情况下非常有用,但也有特殊场景下需要打破该模型。可以通过重写 loadClass()
方法来改变类加载的委托方式。例如,Tomcat 就通过自定义类加载器打破了双亲委派模型,允许 Web 应用优先加载其本地类。
通过自定义类加载器并重写 loadClass()
,可以改变类加载的顺序,使得子加载器先于父加载器进行加载,从而绕过双亲委派模型的限制。例如,Tomcat 为了实现 Web 应用之间的类隔离,定义了不同的类加载器层次结构:
CommonClassLoader
:加载通用类库,供所有 Web 应用使用。CatalinaClassLoader
和SharedClassLoader
:分别加载 Tomcat 自身类和共享类库。WebAppClassLoader
:每个 Web 应用都有独立的类加载器,避免 Web 应用之间的类冲突。
此外,还可以利用线程上下文类加载器(ThreadContextClassLoader
)来解决某些特殊情况。例如,在 Spring 中,线程上下文类加载器可以允许不同的类加载器共享类,从而打破双亲委派模型。
ClassLoader cl = Thread.currentThread().getContextClassLoader();
通过这种方式,Spring 等框架可以根据线程的上下文使用不同的类加载器来加载业务类。