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

9 阅读6分钟

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

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

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

区分加载和初始化

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

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

(1) 使用 new 创建实例

MyClass obj = new MyClass();   // 触发 MyClass 初始化(若尚未)

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

(2)调用类的静态方法

MyClass.staticMethod();        // 触发

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

int x = MyClass.staticField;   // 触发(非编译时常量)
MyClass.staticField = 5;       // 触发

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

int x = MyClass.CONSTANT;      // 不触发初始化

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

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

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

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

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

new ChildClass();    // 先初始化 ParentClass,再 ChildClass

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

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)

运行时生成的字节码也必须通过某个 ClassLoader 的 defineClass() 方法注册到 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方法

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函数获得的。

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

我们再看看SpringApplication的getClassLoader函数

public ClassLoader getClassLoader() {  
    if (this.resourceLoader != null) {  
        return this.resourceLoader.getClassLoader();  
    }  
    return ClassUtils.getDefaultClassLoader();  
}

ClassUtils的getDefaultClassLoader()

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)第二种是,调用到类的静态方法,会触发类的加载。这一场景如下面动图所示。

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

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的加载。

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