从JVM到Spring Boot:一文搞懂胖Jar中的类加载机制

从JVM到Spring Boot:一文搞懂胖Jar中的类加载机制

在使用 Spring Boot 时,你是否好奇过:为什么我们的业务类能够被加载?为什么 META-INF/spring.factories 里的配置能生效?这背后都离不开类加载机制。本文尝试分析Spring Boot的类加载机制

一、从java,jvm层面来看,哪些场景会触发类加载?

区分加载和初始化

加载(Loading) :通过类加载器找到字节码,创建 Class 对象。
链接(Linking) :验证、准备(分配静态字段默认值)、解析符号引用。
初始化(Initialization) :执行静态初始化块和静态字段赋初始值。

1.1初始化的过程中涉及到类加载的场景

(1) 使用 new 创建实例

java 复制代码
MyClass obj = new MyClass();   // 触发 MyClass 初始化(若尚未)

包括直接 new、通过反射 Constructor.newInstance() 等。

(2)调用类的静态方法

java 复制代码
MyClass.staticMethod();        // 触发

(3)访问或设置类的静态字段

java 复制代码
int x = MyClass.staticField;   // 触发(非编译时常量)
MyClass.staticField = 5;       // 触发

例外:如果静态字段是编译时常量static final 且类型是基本类型或字符串,值在编译期确定),不会触发初始化。

java 复制代码
int x = MyClass.CONSTANT;      // 不触发初始化

原因是编译时常量在编译阶段就被内联到调用处,不依赖类加载。

(4)通过反射触发初始化

java 复制代码
Class.forName("com.example.MyClass");  // 默认 initialize=true,触发初始化
Class.forName("com.example.MyClass", false, classLoader); // 只加载不初始化

注意:Class.forNameinitialize 参数控制是否初始化;直接 Class.forName(className)true

(5)初始化子类时,先初始化父类

java 复制代码
new ChildClass();    // 先初始化 ParentClass,再 ChildClass

但通过子类引用父类的静态字段并不会触发子类初始化:

java 复制代码
ChildClass.staticParentField;   // 只初始化 ParentClass,不初始化 ChildClass

(6)作为 JVM 启动类

包含 main 方法的类在启动时会被初始化。

(7)MethodHandle 的解析

当通过 java.lang.invoke.MethodHandle 访问某个类的静态方法或字段时,如果该目标类未初始化,则触发初始化(JDK 7+)。

(8)接口的默认方法

当一个类实现一个接口,且首次调用该接口的 default 方法 时,可能导致接口的初始化(JDK 8+)。

规则:接口初始化只发生在访问了它的非编译时常量的静态字段,或者调用其 default 方法。实现接口本身并不一定触发接口初始化。

二、类加载时都必须由ClassLoader加载

JVM 中的每一个类或接口,都必须通过某个 ClassLoader 加载。只是有些加载器在 Java 层不可见。

2.1 普通类与接口

AppClassLoaderExtClassLoader 或自定义加载器加载,getClassLoader() 返回明确实例。

2.2 核心类库(如 java.lang.String

Bootstrap ClassLoader(引导类加载器) 加载,该加载器由 C++ 实现,Java 层无法获取,因此 String.class.getClassLoader() 返回 null

注意:null 不等于"没有加载器",只是 Java 层看不见。

2.3 数组类

数组类由 JVM 动态创建,没有独立的 ClassLoader 对象。它的类加载器与元素类型 的加载器一致。

例如:String[] 的加载器与 String 相同(Bootstrap,返回 null);MyClass[] 则返回加载 MyClass 的加载器。

2.4 动态生成的类(代理、Lambda、CGLIB)

运行时生成的字节码也必须通过某个 ClassLoaderdefineClass() 方法注册到 JVM。

  • 动态代理:绑定到传入的 ClassLoader
  • Lambda:绑定到函数式接口所在类的加载器
  • CGLIB/ASM:绑定到生成时指定的加载器

2.5 结论

每个类都有定义它的 ClassLoadergetClassLoader() 返回 null 仅表示该类由引导类加载器加载,绝不是"没有加载器"。

三、Springboot胖jar加载类时所用的ClassLoader

如前文所述,Spring Boot的胖 Jar 需要启动器(Launcher),是因为其目录结构不符合 JVM 默认的类路径查找规则,因此必须使用自定义类加载器 LaunchedClassLoader

对于Spring Boot而言,将启动分为两部分。
1. Launcher启动时,类加载器使用默认的类加载器。
2. 在Start-Class的main函数启动后,springboot中除了Launcher包中的文件,其他的类全部由LaunchedClassLoader加载。
下面详述读源码看到的两种典型运行时加载类的代码。

(1)第一种是调用SpringFactoriesLoader的load方法

java 复制代码
public <T> List<T> load(Class<T> factoryType, @Nullable ArgumentResolver argumentResolver) {  
    return load(factoryType, argumentResolver, null);  
}

public <T> List<T> load(Class<T> factoryType, @Nullable ArgumentResolver argumentResolver,  
@Nullable FailureHandler failureHandler) {  
    ...//前面部分代码省略
    for (String implementationName : implementationNames) {  
        //根据类名加载类
        T factory = instantiateFactory(implementationName, factoryType,     argumentResolver, failureHandlerToUse);  
        if (factory != null) {  
            result.add(factory);  
        }  
    }  
    AnnotationAwareOrderComparator.sort(result);  
    return result;  
}

protected <T> T instantiateFactory(String implementationName, Class<T> type,  
@Nullable ArgumentResolver argumentResolver, FailureHandler failureHandler) {  
  
try {  
    // 加载类
    Class<?> factoryImplementationClass = ClassUtils.forName(implementationName, this.classLoader);  
    // 查找构造函数
    FactoryInstantiator<T> factoryInstantiator =FactoryInstantiator.forClass(factoryImplementationClass);  
    // 通过构造函数创建对象
    return factoryInstantiator.instantiate(argumentResolver);     
}

在Class<?> factoryImplementationClass = ClassUtils.forName(implementationName, this.classLoader);这段代码中有一个this.classLoader,它是SpringFactoriesLoader的一个成员变量,是在初始化时,通过springApplication的getClassLoader函数获得的。

java 复制代码
private <T> List<T> getSpringFactoriesInstances(Class<T> type, ArgumentResolver argumentResolver) {  
    return SpringFactoriesLoader.forDefaultResourceLocation(getClassLoader()).load(type, argumentResolver);  
}

我们再看看SpringApplication的getClassLoader函数

java 复制代码
public ClassLoader getClassLoader() {  
    if (this.resourceLoader != null) {  
        return this.resourceLoader.getClassLoader();  
    }  
    return ClassUtils.getDefaultClassLoader();  
}

ClassUtils的getDefaultClassLoader()

java 复制代码
public static ClassLoader getDefaultClassLoader() {  
    ClassLoader cl = null;  
    try {  
        cl = Thread.currentThread().getContextClassLoader();  
    }  
    catch (Throwable ex) {  
    // Cannot access thread context ClassLoader - falling back...  
    }  
    if (cl == null) {  
        // No thread context class loader -> use class loader of this class.  
        cl = ClassUtils.class.getClassLoader();  
    if (cl == null) {  
        // getClassLoader() returning null indicates the bootstrap ClassLoader  
        try {  
            cl = ClassLoader.getSystemClassLoader();  
        }  
        catch (Throwable ex) {  
        // Cannot access system ClassLoader - oh well, maybe the caller can live with null...  
        }  
    }  
}  
return cl;  
}

我们看到第4行,这里就和之前springboot的胖jar启动联系起来,在胖jar的启动中,Launcher会把自定义LauchedClassLoader放到这个currentThread里。

(2)第二种是,调用到类的静态方法,会触发类的加载。这一场景如下面动图所示。

在LogAdapter的isPresent函数中,调用Class.forName函数传入的ClassLoader是通过LogAdapter.class.getClassLoader()获取的。

java 复制代码
private static boolean isPresent(String className) {  
    try {  
        Class.forName(className, false, LogAdapter.class.getClassLoader());  
        return true;  
    }  
    catch (Throwable ex) {  
        // Typically ClassNotFoundException or NoClassDefFoundError...  
        return false;  
    }  
}

那么LogAdapter的ClassLoader是什么呢?我们看LogAdapter是如何加载的,在堆栈中看到LogFactory的getLog方法调用了LogAdapter的静态方法,导致LogAdapter的加载。

java 复制代码
public static Log getLog(String name) {  
    return LogAdapter.createLog(name);  
}

根据jvm的规范,LogAdapter的类加载器是调用它的类(LogFactory)的类加载器,由于从Start-Class的main函数开始,类都是由LaunchedClassLoader加载的,所以,LogFactory也是由LaunchedClassLoader加载的,进而可以推导,几乎所有springboot后面加载的类也是由LaunchedClassLoader加载的。

目前看到了这两种类加载的方式,如果后面看到其他加载类的方式,会继续补充。

总结:

  • JVM 触发类加载的场景主要是"主动使用"(new、静态方法、静态字段等),编译时常量不触发初始化是容易踩的坑。
  • 每个类都必须由某个 ClassLoader 加载,null 仅表示 Bootstrap 加载器。
  • Spring Boot 通过 LaunchedClassLoader 打破双亲委派,实现了从胖 Jar 中加载业务类。
  • 运行时类加载主要通过 SpringFactoriesLoaderClass.forName 完成,它们的 ClassLoader 最终都来自 LaunchedClassLoader
相关推荐
龙码精神13 分钟前
TimescaleDB 物联网设备属性历史数据表设计及常用SQL文档
后端
小小小小宇31 分钟前
Go 后端锁机制详解
后端
挖坑的张师傅34 分钟前
你的仓库 Agent Ready 了吗?
后端
客场消音器1 小时前
如何使用codex进行UI重构,让AI开发的前端页面不再千篇一律
前端·后端·微信小程序
Full Stack Developme1 小时前
spring-beans 解析
java·后端·spring
苏三说技术2 小时前
为什么大厂都不推荐在MySQL中使用NULL值?
后端
techdashen2 小时前
Rust 模块和文件不是一回事:一次讲清 `mod`、`use`、`pub use`
开发语言·后端·rust
爱勇宝2 小时前
别焦虑,也别躺平:给年轻程序员的一封信
前端·后端·架构
Full Stack Developme2 小时前
Spring 发展历史
java·后端·spring
ClouGence2 小时前
TiCDC 够用吗?聊聊 TiDB 同步的几个关键问题
数据库·分布式·后端