JVM学习笔记-----类加载

发布于:2025-08-19 ⋅ 阅读:(11) ⋅ 点赞:(0)

类加载

类加载阶段


加载

  • 将类的字节码载入方法区中,内部采用 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); 
  1. 优先检查缓存:类加载时首先查询已加载类缓存(findLoadedClass),避免重复加载,提升效率,这是类加载的第一道检查机制。
  2. 双亲委派加载:若父加载器时优先委派父加载器加载,无父加载器则尝试 Bootstrap 加载器,通过层级委派保证基础类加载的一致性和安全性,防止类重复定义。
  3. 自身加载兜底:若父加载器均加载失败,当前类加载器调用 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();
        }
    }
}


网站公告

今日签到

点亮在社区的每一天
去签到