从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
相关推荐
Reart2 小时前
从0解构tinyWeb项目--(Day:9)
后端·架构·github
小码哥_常2 小时前
Java后端定时任务“三剑客”大比拼,选对不选贵!
后端
oldking呐呐2 小时前
MySQL从入门到入土 -- 2.数据库基础
后端·mysql
小兵张健2 小时前
30天减20斤挑战:少一斤发100红包(2)
后端·程序员·全栈
汤姆Tom2 小时前
从 0 到 1 开发项目?你是否也是这样开始?先有再优化一步一步带你了解架构设计
前端·后端·架构
muskk6 小时前
一个文件,9万星:Karpathy 用 4 条规则治好了 AI 写代码的"坏毛病"
前端·后端
xlecho7 小时前
从单一语言到全域全栈,AI凭全能实力,淘汰旧时代语言工程师
人工智能·后端·开源
uzong8 小时前
AI 当下,为什么如此焦虑,是怕被替代,还是提前转行,或者保持冷静并做好布局
后端