java类加载机制升职加薪之旅--重点笔记

⼀、快速梳理JAVA类加载机制

三句话总结JDK8的类加载机制:
1. 类缓存:每个类加载器对他加载过的类都有⼀个缓存。
2. 双亲委派:向上委托查找,向下委托加载。
3. 沙箱保护机制:不允许应⽤程序加载JDK内部的系统类。
先来⼀个简单的Demo
public class LoaderDemo {
public static String a ="aaa";
public static void main(String[] args) throws ClassNotFoundException {
// ⽗⼦关系 AppClassLoader <- ExtClassLoader <- BootStrap Classloader
ClassLoader cl1 = LoaderDemo.class.getClassLoader();
System.out.println("cl1 > " + cl1);
System.out.println("parent of cl1 > " + cl1.getParent());
// BootStrap Classloader由C++开发,是JVM虚拟机的⼀部分,本身不是JAVA类。
System.out.println("grant parent of cl1 > " + cl1.getParent().getParent());
// String,Int等基础类由BootStrap Classloader加载。
ClassLoader cl2 = String.class.getClassLoader();
System.out.println("cl2 > " + cl2);
System.out.println(cl1.loadClass("java.util.List").getClass().getClassLoader());
// java指令可以通过增加-verbose:class -verbose:gc 参数在启动时打印出类加载情况
 // 这些参数来⾃于 sun.misc.Launcher 源码
// BootStrap Classloader,加载java基础类。
System.out.println("BootStrap ClassLoader加载⽬录:" +
System.getProperty("sun.boot.class.path"));
// Extention Classloader 加载⼀些扩展类。 可通过-D java.ext.dirs另⾏指定⽬录
System.out.println("Extention ClassLoader加载⽬录:" +
System.getProperty("java.ext.dirs"));
// AppClassLoader 加载CLASSPATH,应⽤下的Jar包。可通过-D java.class.path另⾏指定⽬录
System.out.println("AppClassLoader加载⽬录:" +
System.getProperty("java.class.path"));
 }
}
可以看到JDK8中的两个类加载体系:

image

 

左侧是JDK中实现的类加载器,通过parent属性形成⽗⼦关系。应⽤中⾃定义的类加载器的parent都是AppClassLoader
右侧是JDK中的类加载器实现类。通过类继承的机制形成体系。未来我们就可以通过继承相关的类实现⾃定义类加载器

 

JDK8中的类加载器都继承于⼀个统⼀的抽象类ClassLoader,类加载的核⼼也在这个⽗类中。其中,加载类的核
⼼⽅法如下:
//类加载器的核⼼⽅法
    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) {
          // 父加载器全部找不到,进入下方自身加载逻辑 父类的findClass方法报异常,到catch里,然后执行下面自己的方法。 }
if (c == null) { long t1 = System.nanoTime(); // ⽗类加载起没有加载过,就⾃⾏解析class⽂件加载。 上层所有加载器都找不到,自己执行 findClass 加载(向下回调的本质 c = findClass(name); sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } //这⼀段就是加载过程中的链接Linking部分,分为验证、准备,解析三个部分。 // 运⾏时加载类,默认是⽆法进⾏链接步骤的。 if (resolve) { resolveClass(c); } return c; } }

 

这个⽅法就是最为核⼼的双亲委派机制。并且这个⽅法是protected声明的,这意味着,这个⽅法是可以被⼦类覆盖的。所以,双亲委派机制也是可以被打破的。
当⼀个类加载器要加载⼀个类时,整体的过程就是通过双亲委派机制向上委托查找,如果没有查找到,就向下委托加载。整个过程整理如下图:

image

 

二 沙箱保护机制

双亲委派机制有⼀个最⼤的作⽤就是要保护JDK内部的核⼼类不会被应⽤覆盖。⽽为了保护JDK内部的核⼼类,JAVA在双亲委派的基础上,还加了⼀层保险。
private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)
 {
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
// 不允许加载核⼼类
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
 ("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
pd = defaultDomain;
}
if (name != null) checkCerts(name, pd.getCodeSource());
return pd;
}

 

三 Linking链接过程

在ClassLoader的loadClass⽅法中,还有⼀个不起眼的步骤,resolveClass。这是⼀个native⽅法。⽽其实现的过程称为linking-链接。链接过程的实现功能如下图:

image

 

四 多个点总结

1,URLClassLoader 引入外部jar包
URL jarPath = new
URL("file:/Users/roykingw/DevCode/ClassLoadDemo/out/artifacts/SalaryCaler_jar/SalaryCaler.
jar");
URLClassLoader urlClassLoader = new URLClassLoader(new URL[] {jarPath});

 

2,⾃定义类加载器重写findclass和defindClass方法

3,例如IDEA中的JRebel插件,还有之前介绍过的Arthas。(热加载插件

4,java的SPI机制

public class SPITest {
public static void main(String[] args) {
List<String> names =
SpringFactoriesLoader.loadFactoryNames(ApplicationContextInitializer.class, null);
names.forEach(System.out::println);
System.out.println("==============");
List<ApplicationContextInitializer> applicationContextInitializers =
SpringFactoriesLoader.loadFactories(ApplicationContextInitializer.class, null);
applicationContextInitializers.forEach(System.out::println);
 }
}

 

5,java --classpath 启动类 引入外部jar包。

 

 

Java 自定义类加载器完整教程

一、核心基础

1. 双亲委派模型

类加载器层级: BootstrapClassLoader(启动类加载器)ExtClassLoader(扩展)AppClassLoader(应用)自定义类加载器 加载规则:先委托父加载器加载,父加载器找不到,自己再加载。

2. 自定义类加载器两种方式

  1. 继承 ClassLoader:重写 findClass(String name)(推荐,遵循双亲委派)
  2. 继承 URLClassLoader:适合加载磁盘 / 网络 jar、class 文件

禁止直接重写 loadClass(),会破坏双亲委派。

3. 关键方法

  • loadClass(String name):入口,实现双亲委派逻辑(父类已写好)
  • findClass(String name):父加载器找不到时回调,我们在此读取字节码
  • defineClass(name, byte[] b, off, len):将字节数组转为 Class 对象(核心 API)

二、最简自定义类加载器(继承 ClassLoader)

步骤

  1. 新建类继承 ClassLoader
  2. 重写 findClass,读取 .class 文件字节流
  3. 调用 defineClass 生成 Class
  4. 反射实例化测试

1. 待加载测试类(提前编译出 Test.class)

// Test.java
public class Test {
    public void hello() {
        System.out.println("自定义类加载器加载的Test类");
    }
}

 

编译:javac Test.java 得到 Test.class 文件。

2. 自定义加载器代码

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class MyClassLoader extends ClassLoader {
    // class文件存放目录
    private String classPath;

    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }

    /**
     * 父加载器找不到类时,执行该方法读取字节码
     */
    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        byte[] classBytes = getClassBytes(className);
        if (classBytes == null) {
            throw new ClassNotFoundException("类不存在:" + className);
        }
        // 字节数组转Class对象
        return defineClass(className, classBytes, 0, classBytes.length);
    }

    /**
     * 根据全类名读取磁盘上的.class字节数组
     */
    private byte[] getClassBytes(String className) {
        // 全类名替换路径分隔符:com.demo.Test → com/demo/Test.class
        String path = classPath + File.separator + className.replace(".", File.separator) + ".class";
        File classFile = new File(path);
        if (!classFile.exists()) {
            return null;
        }
        try (FileInputStream fis = new FileInputStream(classFile);
             ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
            byte[] buf = new byte[1024];
            int len;
            while ((len = fis.read(buf)) != -1) {
                bos.write(buf, 0, len);
            }
            return bos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
}

 

3. 测试调用

public class TestLoader {
    public static void main(String[] args) throws Exception {
        // Test.class所在文件夹路径
        MyClassLoader loader = new MyClassLoader("D:/class");
        // 加载全类名
        Class<?> clazz = loader.loadClass("Test");
        // 反射创建对象
        Object obj = clazz.newInstance();
        // 调用方法
        clazz.getMethod("hello").invoke(obj);

        // 打印类加载器
        System.out.println("类加载器:" + clazz.getClassLoader());
    }
}

 


三、破坏双亲委派模型(拓展)

打破方式两种:

1,重写 loadClass() 改写委托逻辑(不推荐,破坏安全);
2,使用线程上下文类加载器 ContextClassLoader(SPI 标准方案)
JDBC 驱动 (JDK 核心包没有数据库驱动实现,Bootstrap/App 加载不到,需要线程上下文类加载器打破双亲,由下层应用加载器加载驱动;)。

 

业务场景:热更新、加载自定义加密 class,需要优先自己加载,不委托父加载器。 重写 loadClass 方法

@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
    // 1. 先查询是否已经加载过
    Class<?> c = findLoadedClass(name);
    if (c == null) {
        try {
            // 不委派父加载器,直接自己加载
            c = findClass(name);
        } catch (ClassNotFoundException e) {
            // 自己找不到再交给父加载器
            super.loadClass(name);
        }
    }
    return c;
}

 


四、URLClassLoader(加载 jar / 网络 class,更常用)

适合加载本地 jar 包、网络远程 class,不用自己写 IO 读取字节码:

import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class URLLoaderDemo {
    public static void main(String[] args) throws Exception {
        // class文件/jar所在路径
        URL url = new URL("file:D:/class/");
        URLClassLoader loader = new URLClassLoader(new URL[]{url});
        Class<?> clazz = loader.loadClass("Test");
        clazz.getDeclaredConstructor().newInstance();
    }
}

 


五、加密 class 文件加载实战(典型场景)

需求:class 文件加密存放,自定义加载器读取后解密再调用defineClass 核心逻辑:

  1. 磁盘读取加密字节数组
  2. 自定义解密算法还原原始 class 字节
  3. 解密后的 byte 数组传入defineClass生成类
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
    byte[] encryptBytes = getEncryptBytes(className);
    // 自定义解密方法
    byte[] realBytes = decrypt(encryptBytes);
    return defineClass(className, realBytes, 0, realBytes.length);
}

 


六、关键注意事项

  1. 双亲委派好处:防止核心类被篡改(如自定义java.lang.String不会被加载)、类全局唯一。
  2. 类的唯一性:同一个类名 + 同一个类加载器才视为同一个 Class;不同加载器加载同名类是两个完全独立类,instanceof 判断为 false。
  3. 线程上下文类加载器Thread.currentThread().getContextClassLoader(),SPI 机制常用,打破双亲委派。
  4. 资源释放URLClassLoader 使用完调用close()释放资源,避免文件句柄泄漏。
  5. 禁止自定义包名:不能加载java.sun.开头的类,安全机制直接拦截。

七、典型使用场景

  1. 热部署 / 热更新(Spring Boot devtools、Tomcat 热加载)
  2. 加密 / 自定义格式 class 文件加载
  3. 动态加载外部 jar 包(插件化架构)
  4. 远程加载网络 class
  5. 隔离不同版本依赖(容器、中间件)

 

 

 

SPI + 线程上下文类加载器 打破双亲委派源码解析(JDBC 驱动场景)

一、背景矛盾:为什么双亲委派加载不了 JDBC 驱动

  1. java.sql.DriverManagerjava.sql.Driverrt.jar 内的核心类,由 Bootstrap 类加载器加载;
  2. MySQL、PostgreSQL 等数据库驱动(com.mysql.cj.jdbc.Driver)是第三方 jar,放在应用 classpath,由 AppClassLoader 加载;

按照双亲委派模型: Bootstrap 加载的 DriverManager 想主动创建驱动实现类,但是 Bootstrap 看不到 AppClassLoader 管理的第三方 jar,上层加载器无法访问下层加载器的类,直接加载会抛 ClassNotFoundException

解决方案:线程上下文类加载器 TCCL(Thread Context ClassLoader),反向拿到下层应用类加载器,实现 “上层代码调用下层类”,打破双亲委派单向向上的限制。

二、关键入口:DriverManager.getConnection () 源码流程

1. 静态块加载驱动(JDK8/9 通用逻辑)

static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

private static void loadInitialDrivers() {
    String drivers;
    try {
        // 读取系统属性 jdbc.drivers
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                return System.getProperty("jdbc.drivers");
            }
        });
    } catch (Exception ex) {
        drivers = null;
    }

    // 核心:通过SPI加载META-INF/services/java.sql.Driver下的驱动实现
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
            // 重点:获取当前线程上下文类加载器
            ClassLoader l = Thread.currentThread().getContextClassLoader();
            // SPI加载器,传入TCCL
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class, l);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();

            try {
                while (driversIterator.hasNext()) {
                    driversIterator.next();
                }
            } catch (Throwable t) {
                // ignore
            }
            return null;
        }
    });

    // 加载系统属性配置的驱动类
    if (drivers != null && !drivers.isEmpty()) {
        String[] driversList = drivers.split(":");
        for (String aDriver : driversList) {
            try {
                // 这里Class.forName默认使用调用方类加载器(Bootstrap),老方式
                Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }
}

 

核心代码片段拆解

// 1. 获取线程上下文类加载器(默认是AppClassLoader)
ClassLoader l = Thread.currentThread().getContextClassLoader();
// 2. SPI指定用TCCL去加载驱动实现类
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class, l);

 

  • Driver.class 是 Bootstrap 加载的接口;
  • 传入的 l 是下层 AppClassLoader;
  • ServiceLoader 会用下层加载器去扫描 classpath 下第三方 jar 里的 META-INF/services/java.sql.Driver 文件,读取驱动全类名并实例化。

三、ServiceLoader.load 如何利用 TCCL 打破双亲

ServiceLoader 核心逻辑

public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
    return new ServiceLoader<>(service, loader);
}

 

内部迭代器 hasNext() / next() 逻辑:

  1. 使用传入的 loader(TCCL=AppClassLoader)读取 classpath 资源文件;
  2. 解析文件中驱动实现类全限定名;
  3. Class.forName(className, true, loader) —— 用下层 AppClassLoader 加载驱动类

正常双亲是上层调下层不行,这里主动把下层加载器传给 SPI 工具类,让上层核心包代码主动委托下层加载器加载第三方实现,实现反向委派

四、TCCL 是什么?为什么能打破双亲

1. 双亲委派单向限制

加载器父子关系单向:自定义 → App → Ext → Bootstrap 只能子委托父,父不能主动访问子加载器,上层看不到下层的类。

2. 线程上下文类加载器作用

每个 Thread 持有一个 contextClassLoader,默认初始化时赋值为应用类加载器 AppClassLoader。 API:

// 获取
ClassLoader tccl = Thread.currentThread().getContextClassLoader();
// 修改(框架热加载、插件场景常用)
Thread.currentThread().setContextClassLoader(loader);

 

它相当于后门:让上层加载的代码,可以拿到一个下层加载器引用,反向加载下层类,绕开双亲向上委托的固定流程。

3. 两种委派对比

  1. 标准双亲委派:加载请求向上传递,父优先;父看不到子类资源;
  2. TCCL 反向委派:上层代码主动拿到子加载器,直接用子加载器加载第三方实现,先下后上,打破原有顺序。

五、老版本 Class.forName ("com.mysql.cj.jdbc.Driver") 原理补充

JDBC4.0 以前必须手动写:

Class.forName("com.mysql.cj.jdbc.Driver");
  1. Class.forName(String className) 无参重载,使用当前调用者的类加载器
  2. 如果在业务代码(App 加载器加载的类)中调用,加载器是 AppClassLoader,可以正常加载驱动;
  3. 如果在 rt.jar 核心类中直接调用 forName,加载器是 Bootstrap,会找不到驱动; 所以 JDBC4.0 之后引入 SPI+TCCL 自动注册驱动,无需手动 Class.forName。

六、驱动注册完整链路

  1. 应用启动,执行 DriverManager 静态代码块;
  2. 获取当前线程 TCCL(AppClassLoader);
  3. ServiceLoader 通过 AppClassLoader 扫描所有 jar 的 META-INF/services/java.sql.Driver
  4. 文件内写死 com.mysql.cj.jdbc.Driver
  5. AppClassLoader 加载驱动类,执行驱动静态代码块;
    // MySQL驱动内部静态代码
    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Register fail");
        }
    }

     

  6. 将驱动实例注册到 Bootstrap 加载的 DriverManager 内部集合;
  7. 后续 getConnection() 遍历已注册驱动,匹配 url 获取连接。

七、总结核心要点

  1. 矛盾根源:Bootstrap 加载的标准接口,实现类在下层 App 加载器管理的第三方 jar,双亲单向访问不到;
  2. 破局关键:线程上下文类加载器 TCCL,给上层核心代码一个下层加载器的引用;
  3. 打破方式:上层主动传入子加载器给 ServiceLoader,由子加载器加载第三方实现类,反向委派;
  4. 适用场景:所有 SPI 场景(JDBC、日志 SLF4J、Dubbo SPI、Spring SPI)均依赖 TCCL 打破双亲委派;
  5. 区别于重写 loadClass 破坏双亲:TCCL 是 JDK 提供的标准、安全的反向加载方案,不会破坏全局类加载安全机制。

 

 

 

 

 

 

 

 

 

 

posted @ 2026-04-01 13:30  OMGq  阅读(8)  评论(0)    收藏  举报