类加载
类加载阶段
加载
- 将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
- _java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用
- _super 即父类
- _fields 即成员变量
- _methods 即方法
- _constants 即常量池
- _class_loader 即类加载器
- _vtable 虚方法表
- _itable 接口方法表
- 如果这个类还有父类没有加载,先加载父类
- 加载和链接可能是交替运行的
注意
instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror 是存储在堆中
可以通过前面介绍的 HSDB 工具查看
Person.class
的字节码被加载到 JVM 的方法区,JVM 会基于其中的信息创建 instanceKlass
等内部结构来管理类
链接
验证
验证类是否符合JVM规范,做一些安全性检查
准备
为 static 变量分配空间,设置默认值
- static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
- static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
- 如果 static 变量是 final 的基本类型,那么编译阶段值就确定了,赋值在准备阶段完成
- 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
静态变量在堆中跟类对象存储在一起
解析
将常量池中的符号引用解析为直接引用
符号引用仅仅就只是一个符号,它并不知道这些符号在哪个内存的位置,但经过解析以后变成直接引用就能确切的找到类/方法等在内存中的位置
初始化
<clinit>() V 方法
初始化即调用<clinit>() V,虚拟机会保证这个类的『构造方法』的线程安全
发生的时机
概括得说,类初始化是【懒惰的】
- main 方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法时
- 子类初始化,如果父类还没初始化,会引发
- 子类访问父类的静态变量,只会触发父类的初始化
- Class.forName
- new 会导致初始化
不会导致类初始化的情况
- 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化(准备阶段)
- 类对象.class 不会触发初始化
- 创建该类的数组不会触发初始化
- 类加载器的 loadClass 方法
- Class.forName的参数2为false时
类加载器
以JDK8为例:
名称 | 加载哪的类 | 说明 |
---|---|---|
Bootstrap ClassLoader | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader | JAVA_HOME/jre/lib/ext | 上级为 Bootstrap,显示为 null |
Application ClassLoader | classpath | 上级为 Extension |
自定义类加载器 | 自定义 | 上级为 Application |
各司其职,各管一块
Application ClassLoader去加载类的时候首先会问一问看一下这个类是不是由它的上级Extension ClassLoader加载过了,如果没有它还会问问它的上级Bootstrap ClassLoader是否加载过了,如果它的两个上级都没有加载那才轮的到Application ClassLoader去加载。
这种类的委托方式被称为双亲委派的类加载方式
启动类加载器
C++编写
扩展类加载器
应用程序类加载器
双亲委派模式
所谓的双亲委派,就是指调用类加载器的loadclass方法时,查找类的规则。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 加锁:保证同一时刻,一个类只会被一个线程加载,避免重复加载或加载冲突
synchronized (getClassLoadingLock(name)) {
// 1. 检查该类是否 **已经被加载过**
// 类加载器内部维护已加载类缓存,先查缓存,避免重复加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime(); // 记录开始时间,用于性能统计
try {
if (parent != null) {
// 2. 有父加载器:委派 **父加载器** 尝试加载
// 体现 “双亲委派” 机制,优先让父加载器处理,保证基础类由更上层加载
c = parent.loadClass(name, false);
} else {
// 3. 无父加载器(如 ExtClassLoader ,其 “父” 是 Bootstrap ,用特殊方式处理)
// 尝试让 Bootstrap ClassLoader 加载(Bootstrap 是 C++ 实现,Java 层显式调用特殊方法)
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父加载器加载失败:这里不处理,后续走自己的 findClass 逻辑
}
// 4. 如果父加载器 **层层委派都没找到** ,调用当前加载器的 findClass 加载
if (c == null) {
long t1 = System.nanoTime();
// findClass 是模板方法,子类(如自定义类加载器)可重写,实现自定义加载逻辑(比如从网络、加密文件加载)
c = findClass(name);
// 5. 性能统计:记录类加载耗时、次数等(JVM 内部性能监控用)
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
// 6. 如果需要解析(resolve=true),进行类的解析:把符号引用转为直接引用,准备静态变量、字节码验证等
if (resolve) {
resolveClass(c);
}
return c;
}
}
// 模板方法:默认实现抛异常,让子类(如自定义类加载器)按需重写,实现自定义加载逻辑
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
// 关键辅助方法:获取类加载锁,保证同一类加载的线程安全
protected Object getClassLoadingLock(String className) {
// 简单实现:通常用 ConcurrentHashMap 存锁,保证一个类对应一把锁
return this;
}
// 访问 Bootstrap ClassLoader 的特殊方法(实际是 JVM 内部实现,这里简化示意)
private native Class<?> findBootstrapClassOrNull(String name);
- 优先检查缓存:类加载时首先查询已加载类缓存(
findLoadedClass
),避免重复加载,提升效率,这是类加载的第一道检查机制。 - 双亲委派加载:若父加载器时优先委派父加载器加载,无父加载器则尝试 Bootstrap 加载器,通过层级委派保证基础类加载的一致性和安全性,防止类重复定义。
- 自身加载兜底:若父加载器均加载失败,当前类加载器调用
findClass
方法自行加载,该方法为模板方法,支持子类重写实现自定义加载逻辑(如从网络、加密文件加载)。
以下是对双亲委派机制优势总结:
优势 | |
---|---|
保证 Java 核心库安全性 | 核心库(如 java.lang.Object )由启动类加载器加载,其基于 JVM 本地代码实现,加载路径固定,避免核心类被篡改,保障系统安全稳定 |
避免类重复加载 | 若类已被父类加载器加载,子类加载器不再重复加载,减少冗余,提升类加载效率,降低内存消耗 |
保证类加载一致性 | 确保同一类在 JVM 中仅有一个定义,规避类冲突。如应用与第三方库类名相同时,优先加载高层次类加载器中的类 |
提高类加载效率 | 类加载请求向上委派,父类加载器若加载过该类,直接返回引用,减少重复加载开销 |
支持动态扩展 | 不同类加载器分工加载不同类,像应用类加载器加载应用特定类、扩展类加载器加载扩展库类,便于动态扩展与模块化开发 |
线程上下文类加载器
使用 JDBC 时,早年需显式调用 Class.forName("com.mysql.jdbc.Driver")
加载驱动,如今不写这句代码,驱动仍可能自动加载。核心疑问:谁在 “暗中” 触发了 Driver 类的加载?
JDBC 核心类 DriverManager
中有静态代码块,会在类加载时自动执行:
public class DriverManager {
// 注册驱动的集合
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
// 初始化驱动(类加载阶段执行)
static {
loadInitialDrivers(); // 关键方法:触发驱动自动加载
println("JDBC DriverManager initialized");
}
}
DriverManager是属于启动类路径下的,它的类加载器是Bootstrap ClassLoader,会到 JAVA_HOME/jre/lib 下搜索类,但 JAVA_HOME/jre/lib 下显然没有 mysql-connector-java-5.1.47.jar 包,这样问题来了,在 DriverManager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver 呢?
private static void loadInitialDrivers() {
String drivers = null;
// 1. 从系统属性 `jdbc.drivers` 读取驱动类名(SPI 加载的补充)
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// 1) 使用 ServiceLoader(SPI 机制)加载驱动
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try {
while (driversIterator.hasNext()) {
// 关键:触发类加载 + 自动注册
driversIterator.next();
}
} catch (Throwable t) {
// 捕获异常,不中断流程
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
// 2) 使用 `jdbc.drivers` 定义的驱动名,通过系统类加载器加载
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
// 使用系统类加载器(Application ClassLoader)加载
Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
jdk在某些情况下打破双亲委派机制,调用系统类加载器,否则有些类是找不到的。
自定义类加载器
什么时候需要自定义类加载器
1)想加载非 classpath 随意路径中的类文件
2)都是通过接口来使用实现,希望解耦时,常用在框架设计
3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器
步骤:
1. 继承 ClassLoader 父类
2. 要遵从双亲委派机制,重写 findClass 方法 注意不是重写 loadClass 方法,否则不会走双亲委派机制
3. 读取类文件的字节码
4. 调用父类的 defineClass 方法来加载类
5. 使用者调用该类加载器的 loadClass 方法
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* 自定义类加载器实现
* 遵循双亲委派机制,用于加载指定路径的类文件
*/
public class CustomClassLoader extends ClassLoader {
// 类文件所在的基础路径(可根据实际需求修改)
private String basePath;
public CustomClassLoader(String basePath) {
// 调用父类构造方法,确保双亲委派链完整
super();
this.basePath = basePath;
}
/**
* 步骤2:重写findClass方法(核心)
* 不重写loadClass,保证双亲委派机制生效
* 当父加载器无法加载类时,会自动调用此方法
*/
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
try {
// 步骤3:读取类文件的字节码
byte[] classData = loadClassData(className);
if (classData == null) {
throw new ClassNotFoundException("类字节码读取失败: " + className);
}
// 步骤4:调用父类的defineClass方法加载类
// 该方法将字节码转换为Class对象,是类加载的关键步骤
return defineClass(className, classData, 0, classData.length);
} catch (IOException e) {
throw new ClassNotFoundException("加载类时发生IO异常: " + className, e);
}
}
/**
* 读取类文件的字节数组(步骤3的具体实现)
* @param className 类的全限定名(如com.example.Test)
* @return 类文件的字节数组
* @throws IOException 读取文件时可能发生的异常
*/
private byte[] loadClassData(String className) throws IOException {
// 将类名转换为文件路径(如com.example.Test -> com/example/Test.class)
String path = basePath + "/" + className.replace('.', '/') + ".class";
try (InputStream is = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int len;
// 读取类文件内容到字节数组输出流
while ((len = is.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
return baos.toByteArray();
}
}
/**
* 使用示例(步骤5:使用者调用loadClass方法)
*/
public static void main(String[] args) {
try {
// 创建自定义类加载器,指定类文件所在的基础路径
CustomClassLoader classLoader = new CustomClassLoader("/path/to/classes");
// 步骤5:调用loadClass方法(继承自父类ClassLoader)
// 该方法会先触发双亲委派机制,父加载器无法加载时才调用自定义的findClass
Class<?> clazz = classLoader.loadClass("com.example.User");
// 验证加载结果
System.out.println("类加载器: " + clazz.getClassLoader()); // 输出自定义类加载器
Object instance = clazz.newInstance();
System.out.println("实例创建成功: " + instance);
} catch (Exception e) {
e.printStackTrace();
}
}
}