从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.forName 的 initialize 参数控制是否初始化;直接 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 普通类与接口
由 AppClassLoader、ExtClassLoader 或自定义加载器加载,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)
运行时生成的字节码也必须通过某个 ClassLoader 的 defineClass() 方法注册到 JVM。
- 动态代理:绑定到传入的
ClassLoader - Lambda:绑定到函数式接口所在类的加载器
- CGLIB/ASM:绑定到生成时指定的加载器
2.5 结论
每个类都有定义它的 ClassLoader ,getClassLoader() 返回 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 中加载业务类。 - 运行时类加载主要通过
SpringFactoriesLoader和Class.forName完成,它们的ClassLoader最终都来自LaunchedClassLoader。