JVM学习笔记-----类加载
类加载
类加载阶段
加载
- 将类的字节码载入方法区中,内部采用 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,保证双亲委派机制生效* 当父加载器无法加载类时,会自动调用此方法*/@Overrideprotected 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)// 该方法会先触发双亲委派机制,父加载器无法加载时才调用自定义的findClassClass<?> clazz = classLoader.loadClass("com.example.User");// 验证加载结果System.out.println("类加载器: " + clazz.getClassLoader()); // 输出自定义类加载器Object instance = clazz.newInstance();System.out.println("实例创建成功: " + instance);} catch (Exception e) {e.printStackTrace();}}
}