1. 概述

本文将深入探讨 Java 何时会抛出 ExceptionInInitializerError 异常。

我们先从理论入手,再结合多个实际代码示例,帮助你彻底理解这个异常的触发机制和背后的 JVM 行为。这类异常在项目启动或类加载阶段出现时往往令人困惑,掌握其原理有助于快速定位“类初始化失败”类问题——这在 Spring 初始化、工具类静态变量加载等场景中并不少见,属于值得掌握的踩坑知识点

2. 什么是 ExceptionInInitializerError

核心定义ExceptionInInitializerError 表示 JVM 在执行静态初始化(static initializer)时发生了未预期的异常。

具体来说,当出现以下任一情况时,JVM 会自动抛出该异常:

  • 静态代码块(static {})执行失败
  • 静态变量初始化过程中抛出异常

⚠️ 关键机制:JVM 会将原始异常封装进 ExceptionInInitializerError,并通过 cause 保留原始异常的引用。这意味着你看到的堆栈信息中,Caused by 后面的内容才是真正的“罪魁祸首”。

理解这一点非常重要——不要被 ExceptionInInitializerError 本身迷惑,重点看它的 cause

3. 静态代码块初始化失败

我们通过一个简单的除零操作来模拟失败:

public class StaticBlock {

    private static int state;

    static {
        state = 42 / 0;
    }
}

当你尝试触发类加载,例如:

new StaticBlock();

将抛出如下异常:

java.lang.ExceptionInInitializerError
    at com.baeldung...(ExceptionInInitializerErrorUnitTest.java:18)
Caused by: java.lang.ArithmeticException: / by zero
    at com.baeldung.StaticBlock.<clinit>(ExceptionInInitializerErrorUnitTest.java:35)
    ... 23 more

🔍 堆栈分析:

  • 外层异常是 ExceptionInInitializerError
  • 内层 Caused by 明确指出:ArithmeticException: / by zero
  • <clinit> 是 JVM 为类生成的类初始化方法,对应你的静态初始化逻辑

使用 AssertJ 验证:

assertThatThrownBy(StaticBlock::new)
  .isInstanceOf(ExceptionInInitializerError.class)
  .hasCauseInstanceOf(ArithmeticException.class);

4. 静态变量初始化失败

静态变量的初始化失败也会触发同样的异常:

public class StaticVar {

    private static int state = initializeState();

    private static int initializeState() {
        throw new RuntimeException();
    }
}

执行:

new StaticVar();

异常输出:

java.lang.ExceptionInInitializerError
    at com.baeldung...(ExceptionInInitializerErrorUnitTest.java:11)
Caused by: java.lang.RuntimeException
    at com.baeldung.StaticVar.initializeState(ExceptionInInitializerErrorUnitTest.java:26)
    at com.baeldung.StaticVar.<clinit>(ExceptionInInitializerErrorUnitTest.java:23)
    ... 23 more

同样,原始异常(RuntimeException)被正确保留:

assertThatThrownBy(StaticVar::new)
  .isInstanceOf(ExceptionInInitializerError.class)
  .hasCauseInstanceOf(RuntimeException.class);

5. 检查型异常(Checked Exception)的处理规范

根据 Java 语言规范(JLS),静态初始化块或静态变量初始化器中不能直接抛出检查型异常

例如以下代码无法通过编译:

public class NoChecked {
    static {
        throw new Exception(); // ❌ 编译错误
    }
}

编译器报错:

java: initializer must be able to complete normally

5.1 正确做法:手动封装为 ExceptionInInitializerError

标准实践:如果你的静态初始化逻辑可能抛出检查型异常,应主动捕获并封装为 ExceptionInInitializerError

public class CheckedConvention {

    private static Constructor<?> constructor;

    static {
        try {
            constructor = CheckedConvention.class.getDeclaredConstructor();
        } catch (NoSuchMethodException e) {
            throw new ExceptionInInitializerError(e);
        }
    }
}

📌 关键点:

  • getDeclaredConstructor() 抛出 NoSuchMethodException(检查型异常)
  • 我们捕获后,显式抛出 ExceptionInInitializerError
  • 此时 JVM 不会再做二次包装,堆栈清晰

5.2 错误示范:包装成 RuntimeException

如果你错误地将其包装为 RuntimeException

static {
    try {
        constructor = CheckedConvention.class.getConstructor();
    } catch (NoSuchMethodException e) {
        throw new RuntimeException(e); // ❌ 不推荐
    }
}

JVM 会认为这是一个未处理的异常,于是再次包装ExceptionInInitializerError,导致堆栈变深:

java.lang.ExceptionInInitializerError
    at com.baeldung.exceptionininitializererror...
Caused by: java.lang.RuntimeException: java.lang.NoSuchMethodException: ...
Caused by: java.lang.NoSuchMethodException: com.baeldung.CheckedConvention.<init>()
    at java.base/java.lang.Class.getConstructor0(Class.java:3427)
    at java.base/java.lang.Class.getConstructor(Class.java:2165)

⚠️ 后果:异常堆栈多了一层,排查时容易被误导。

5.3 真实案例:OpenJDK 源码中的实践

该规范不仅存在于理论,OpenJDK 自身也在广泛使用。例如 AtomicReference 的源码:

public class AtomicReference<V> implements java.io.Serializable {
    private static final VarHandle VALUE;
    static {
        try {
            MethodHandles.Lookup l = MethodHandles.lookup();
            VALUE = l.findVarHandle(AtomicReference.class, "value", Object.class);
        } catch (ReflectiveOperationException e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    private volatile V value;

   // omitted
}

可以看到,JDK 开发者也遵循了这一最佳实践,确保类初始化异常清晰可追溯。

6. 总结

  • ExceptionInInitializerError 是 JVM 对静态初始化失败的统一包装
  • 真正的问题藏在 Caused by 中,务必逐层查看
  • 静态块或静态变量中若涉及检查型异常,应主动封装为 ExceptionInInitializerError
  • 避免包装成 RuntimeException,否则会引发二次包装,污染堆栈
  • 该机制在 JDK 源码中有广泛应用,属于高级开发者应掌握的底层知识

示例代码已上传至 GitHub:https://github.com/yourname/java-exception-tutorial


原始标题:When Does Java Throw the ExceptionInInitializerError?