1. 前言

一次遇到一个问题,在调用Java静态方法时,出现了NoClassDefFoundError异常,通常该异常在找不到类时才会出现,实际上对应的类就在当前项目中,对这个比较奇怪的异常进行了分析。

以下使用的JDK版本为JDK8。

2. 用于验证的关键代码

为了分析以上异常,编写代码进行模拟,以下为被调用类的关键代码,在TestCalleeA类中定义了静态方法与静态变量。

public class TestCalleeA {
    public static String S1 = null;
    public static final String S2 = null;
    public static final int I1 = 1111;
    public static final int I2 = 2222 / (I1 - 1111);
    public static final int I3 = 3333;
    public static String S3 = "S3_value_a";
    public static final String S4 = S2 + "-";
    public static final String S5 = "S5_value";

    static {
        S3 = "S3_value_b";
    }

    public static void empty() {
    }
}

按照以下顺序,调用以上类中的方法,或使用其中的静态变量(在调用类中使用最大线程数为1的线程池串行执行,在出现异常时进行异常处理,并打印异常日志,因此某一步出现异常时后续代码还能继续执行):

TestCalleeA.empty();
TestCalleeA.empty();
System.identityHashCode(TestCalleeA.S1);
System.identityHashCode(TestCalleeA.S2);
System.identityHashCode(TestCalleeA.I1);
System.identityHashCode(TestCalleeA.I2);
System.identityHashCode(TestCalleeA.I3);
System.identityHashCode(TestCalleeA.S3);
TestCalleeA.S3 = "S3_value_set";
System.identityHashCode(TestCalleeA.S4);
System.identityHashCode(TestCalleeA.S5);

3. 验证代码执行结果

在调用类中使用以上被调用类TestCalleeA中的方法或变量时,执行结果如下:

  • TestCalleeA类中只有I1、I3、S5静态变量能够正常访问,对静态方法与其他静态变量的操作均会出现异常;
  • 第一次与第二次调用TestCalleeA.empty()方法时,出现的异常不相同;
  • 第一次调用TestCalleeA.empty()方法时,出现的异常为:java.lang.ExceptionInInitializerError;
  • 第二次调用TestCalleeA.empty()方法,及访问静态变量时,出现的异常为:java.lang.NoClassDefFoundError: Could not initialize class TestCalleeA。

4. 问题分析

4.1. 获取class文件的字节码

需要根据以上代码的字节码分析以上现象,可使用以下命令对class文件进行反汇编,生成对应的字节码:

javap -l -v -p xxx.class > yyy

4.2. 被调用类的方法的字节码

Java类或接口的初始化方法为<clinit>,Java类中的静态变量在定义时的初始化操作,或static代码块中的操作,都会编译到<clinit>方法中。

被调用类TestCalleeA的<clinit>方法的字节码如下:

0: aconst_null
 1: putstatic     #2                  // Field S1:Ljava/lang/String;
 4: aconst_null
 5: putstatic     #3                  // Field S2:Ljava/lang/String;
 8: sipush        2222
11: iconst_0
12: idiv
13: putstatic     #5                  // Field I2:I
16: ldc           #6                  // String S3_value_a
18: putstatic     #7                  // Field S3:Ljava/lang/String;
21: new           #8                  // class java/lang/StringBuilder
24: dup
25: invokespecial #9                  // Method java/lang/StringBuilder."<init>":()V
28: getstatic     #3                  // Field S2:Ljava/lang/String;
31: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
34: ldc           #11                 // String -
36: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
39: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
42: putstatic     #13                 // Field S4:Ljava/lang/String;
45: ldc           #14                 // String S3_value_b
47: putstatic     #7                  // Field S3:Ljava/lang/String;
50: return

可以看到,被调用类TestCalleeA的<clinit>方法中,有对以下静态变量进行赋值:

S1
S2
I2
S3
S4

TestCalleeA类的静态变量I2在初始化时会执行除以零的操作,会导致<clinit>方法执行异常。

4.3. 被调用类中显示为ConstantValue的变量

在TestCalleeA类的字节码中,以下变量显示为ConstantValue,说明以下变量的值为常量:

I1
I3
S5

在被调用类TestCalleeA的<clinit>方法中,没有对以上值为常量的变量进行操作的指令。

4.4. 调用类访问被调用类方法、变量时的指令

查看调用类对应方法的字节码,访问被调用类方法、变量时的指令如下所示:

被调用类中被访问的方法、变量

调用类访问时使用的指令

指令的说明

调用TestCalleeA.empty()静态方法

invokestatic #46

Method TestCalleeA.empty:()V

读取TestCalleeA.S1静态变量

getstatic #45

Field TestCalleeA.S1:Ljava/lang/String;

读取TestCalleeA.S2静态变量

getstatic #44

Field TestCalleeA.S2:Ljava/lang/String;

读取TestCalleeA.I1静态变量

sipush 1111

读取TestCalleeA.I2静态变量

getstatic #43

Field TestCalleeA.I2:I

读取TestCalleeA.I3静态变量

sipush 3333

读取TestCalleeA.S3静态变量

getstatic #41

Field TestCalleeA.S3:Ljava/lang/String;

修改TestCalleeA.S3静态变量

putstatic #41

Field TestCalleeA.S3:Ljava/lang/String;

读取TestCalleeA.S4静态变量

getstatic #39

Field TestCalleeA.S4:Ljava/lang/String;

读取TestCalleeA.S5静态变量

ldc #37

String S5_value

4.5. ExceptionInInitializerError、NoClassDefFoundError出现原因

在类的初始化过程中出现ExceptionInInitializerError异常;使用类的静态方法、变量时出现NoClassDefFoundError异常,这两者有关联。

4.5.1. Java Virtual Machine Specification中的说明


在调用getstatic、putstatic或invokestatic指令时,被调用的方法或变量所在的类若未完成初始化,则会被初始化。

在对类进行初始化时,若类的Class对象处于错误的状态,则无法完成初始化,抛出NoClassDefFoundError异常。

在对类进行初始化时,若类的Class对象未处于错误的状态,且当前线程或其他线程没有同时在对当前类执行初始化操作,则尝试进行初始化。在类的初始化过程中,若抛出了异常E,则需要终止初始化操作:若异常E不是Error类或其子类,则创建一个新的ExceptionInInitializerError实例,以E作为其参数(cause);将当前类的Class对象标记为错误的状态,并抛出异常(E或ExceptionInInitializerError)以终止初始化操作。

在“Chapter 6. The Java Virtual Machine Instruction Set”https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html 中,getstatic、putstatic、invokestatic指令的说明中,也提到了在调用以上指令时,假如会触发被调用类的初始化操作,可能出现异常,需要参考“5.5. Initialization”。

4.5.2. 访问被调用类静态方法、变量出现异常的原因

  • 第一次调用TestCalleeA.empty()方法

在调用类中第一次调用被调用类TestCalleeA.empty()方法时,调用类中对应的指令为invokestatic,会尝试对被调用类TestCalleeA进行初始化;此时TestCalleeA类未完成初始化,invokestatic指令会触发TestCalleeA类的初始化操作;

对TestCalleeA类进行初始化时,会调用其<clinit>方法;

在TestCalleeA类的<clinit>方法中,对I2进行赋值时,会因为除以零出现ArithmeticException异常,不属于Error类或其子类,因此会再创建ExceptionInInitializerError实例,以ArithmeticException异常作为其cause,再将ExceptionInInitializerError异常抛出;

此时TestCalleeA类进行初始化时出现了异常,其Class对象会被标记为错误的状态;

因此调用类第一次调用被调用类TestCalleeA.empty()方法会出现ExceptionInInitializerError异常。

  • 第二次调用TestCalleeA.empty()方法

在调用类中第二次调用被调用类TestCalleeA.empty()方法时,对应的指令为invokestatic,会尝试对TestCalleeA类进行初始化;

此时TestCalleeA类的Class对象已被标记为错误的状态,无法完成初始化,抛出NoClassDefFoundError异常;

因此第二次调用TestCalleeA.empty()方法会出现NoClassDefFoundError异常。

  • 访问TestCalleeA中的静态变量

在调用类中访问被调用类TestCalleeA中的静态变量时,会使用getstatic或putstatic指令。

后续过程与第二次调用TestCalleeA.empty()方法类似,说明略。

4.6. 访问被调用类其他静态变量未出现异常的原因

访问被调用类TestCalleeA其他静态变量时,未出现异常,原因如下:

在调用类方法中访问被调用类的部分静态变量时,使用的指令是sipush或ldc,即相关的静态变量值为常量,不需要使用getstatic、putstatic或invokestatic指令,因此不会出现异常。

以下情况下,在调用类中访问被调用类的对应静态变量时,变量值为常量,不需要使用getstatic、putstatic或invokestatic指令:

  • 被调用类中的静态变量类型为基本类型boolean、byte、char、short、int、long、float、double,变量为static final;
  • 被调用类中的静态变量类型为String,变量为static final,且值非null。

5. Java类静态变量初始化建议

对于静态变量的初始化操作,假如是正常情况下必须成功的操作,可以放在应用初始化时进行初始化,使其初始化异常时,能够使Java进程无法启动,提前发现问题。例如将密码解密的操作作为Spring的Bean,而不是作为类的静态变量的初始化操作。

其他情况下,对于静态变量的初始化操作,如果可能出现异常的操作,可在被调用类中提供一个空的初始化方法。

在调用类中,使用被调用类中的方法或变量之前,先调用对应的空的初始化方法,并进行异常捕获,可在被调用类初始化失败时进行相应的异常处理,例如发送告警信息等,避免出现无法预测的结果。

类的静态初始化过程出现的异常类型为ExceptionInInitializerError,不是Exception的子类,因此在调用类中需要对Throwable类型进行异常捕获。

以上所述的示例代码如下

// 被调用类
public class TestCalleeB {
    public static final int I1 = 1111;
    public static final int I2 = 2222 / (I1 - 1111);

    public static void init() {
    }
}

// 调用类
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TestCallerB {
    private static final Logger logger = LoggerFactory.getLogger(TestCallerB.class);

    public static void main(String[] args) throws InterruptedException {
        try {
            TestCalleeB.init();
        } catch (Throwable e) {
            logger.error("TestCallerB初始化异常 ", e);
            return;
        }

        logger.info("TestCalleeB.I1 {}", TestCalleeB.I1);
    }
}

以上类在执行时,日志示例如下:

2022-06-12 22:24:35.032 [main] ERROR TestCallerB(TestCallerB.java:11) -  TestCallerB初始化异常 
java.lang.ExceptionInInitializerError: null
	at TestCallerB.main(TestCallerB.java:9) [classes/:?]
Caused by: java.lang.ArithmeticException: / by zero
	at TestCalleeB.<clinit>(TestCalleeB.java:3) ~[classes/:?]
	... 1 more

6. 类初始化失败时抛出非ExceptionInInitializerError异常的验证

假如希望类初始化失败时抛出非ExceptionInInitializerError异常,参考Java Virtual Machine Specification中的说明,可在代码中抛出Error或其子类类型的异常,对应代码如下

// 被调用类
public class TestCalleeC {
    public static final int I1 = 1111;
    public static int I2;

    static {
        try {
            I2 = 2222 / (I1 - 1111);
        } catch (Exception e) {
            throw new UnsupportedClassVersionError("test message");
        }
    }

    public static void init() {
    }
}

// 调用类
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TestCallerC {
    private static final Logger logger = LoggerFactory.getLogger(TestCallerC.class);

    public static void main(String[] args) throws InterruptedException {
        try {
            TestCalleeC.init();
        } catch (Throwable e) {
            logger.error("TestCalleeC初始化异常 ", e);
        }
    }
}

以上类在执行时,日志示例如下:

2022-06-12 22:23:34.765 [main] ERROR TestCallerC(TestCallerC.java:11) -  TestCalleeC初始化异常 
java.lang.UnsupportedClassVersionError: test message
	at TestCalleeC.<clinit>(TestCalleeC.java:9) ~[classes/:?]
	at TestCallerC.main(TestCallerC.java:9) [classes/:?]

可以看到被调用类TestCalleeC初始化失败时,抛出的异常类型为代码中指定的UnsupportedClassVersionError,而不是默认的ExceptionInInitializerError。

7. 用于验证的调用类代码及执行日志

7.1. 用于验证的调用类代码

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class TestCallerA {
    private static final Logger logger = LoggerFactory.getLogger(TestCallerA.class);

    private static String logFlag;

    private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1),
            r -> {
                Thread thread = new Thread(r);
                thread.setDaemon(false);
                thread.setUncaughtExceptionHandler((t, e) -> {
                    synchronized (TestCallerA.class) {
                        if (e.getCause() == null) {
                            logger.error("[{}] error {}\n\t{}\n", logFlag, e, StringUtils.join(e.getStackTrace(), "\n\t"));
                        } else {
                            logger.error("[{}] error {}\n\t{}\nCaused by: {}\n\t{}\n", logFlag, e, StringUtils.join(e.getStackTrace(), "\n\t"),
                                    e.getCause(), StringUtils.join(e.getCause().getStackTrace(), "\n\t"));
                        }
                    }
                });
                return thread;
            }, new ThreadPoolExecutor.AbortPolicy());

    private static void run(String flag, Runnable r) throws InterruptedException {
        logFlag = flag;
        threadPoolExecutor.execute(() -> {
            r.run();
            logger.info("success [{}]\n", logFlag);
        });

        while (threadPoolExecutor.getActiveCount() > 0 || !threadPoolExecutor.getQueue().isEmpty()) {
            Thread.sleep(10L);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        run("TestCalleeA.empty()-1", () -> TestCalleeA.empty());
        run("TestCalleeA.empty()-2", () -> TestCalleeA.empty());
        run("TestCalleeA.S1", () -> System.identityHashCode(TestCalleeA.S1));
        run("TestCalleeA.S2", () -> System.identityHashCode(TestCalleeA.S2));
        run("TestCalleeA.I1", () -> System.identityHashCode(TestCalleeA.I1));
        run("TestCalleeA.I2", () -> System.identityHashCode(TestCalleeA.I2));
        run("TestCalleeA.I3", () -> System.identityHashCode(TestCalleeA.I3));
        run("TestCalleeA.S3", () -> System.identityHashCode(TestCalleeA.S3));
        run("TestCalleeA.S3 set", () -> TestCalleeA.S3 = "S3_value_set");
        run("TestCalleeA.S4", () -> System.identityHashCode(TestCalleeA.S4));
        run("TestCalleeA.S5", () -> System.identityHashCode(TestCalleeA.S5));

        threadPoolExecutor.shutdown();
    }
}

7.2. 用于验证的调用类代码的执行日志

2022-06-11 22:14:13.125 [Thread-1] ERROR TestCallerA(TestCallerA.java:23) -  [TestCalleeA.empty()-1] error java.lang.ExceptionInInitializerError
	TestCallerA.lambda$main$3(TestCallerA.java:44)
	TestCallerA.lambda$run$2(TestCallerA.java:34)
	java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.ArithmeticException: / by zero
	TestCalleeA.<clinit>(TestCalleeA.java:5)
	TestCallerA.lambda$main$3(TestCallerA.java:44)
	TestCallerA.lambda$run$2(TestCallerA.java:34)
	java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	java.lang.Thread.run(Thread.java:748)

2022-06-11 22:14:13.125 [Thread-2] ERROR TestCallerA(TestCallerA.java:21) -  [TestCalleeA.empty()-2] error java.lang.NoClassDefFoundError: Could not initialize class TestCalleeA
	TestCallerA.lambda$main$4(TestCallerA.java:45)
	TestCallerA.lambda$run$2(TestCallerA.java:34)
	java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	java.lang.Thread.run(Thread.java:748)

2022-06-11 22:14:13.141 [Thread-3] ERROR TestCallerA(TestCallerA.java:21) -  [TestCalleeA.S1] error java.lang.NoClassDefFoundError: Could not initialize class TestCalleeA
	TestCallerA.lambda$main$5(TestCallerA.java:46)
	TestCallerA.lambda$run$2(TestCallerA.java:34)
	java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	java.lang.Thread.run(Thread.java:748)

2022-06-11 22:14:13.157 [Thread-4] ERROR TestCallerA(TestCallerA.java:21) -  [TestCalleeA.S2] error java.lang.NoClassDefFoundError: Could not initialize class TestCalleeA
	TestCallerA.lambda$main$6(TestCallerA.java:47)
	TestCallerA.lambda$run$2(TestCallerA.java:34)
	java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	java.lang.Thread.run(Thread.java:748)

2022-06-11 22:14:13.173 [Thread-5] INFO  TestCallerA(TestCallerA.java:35) -  success [TestCalleeA.I1]

2022-06-11 22:14:13.189 [Thread-5] ERROR TestCallerA(TestCallerA.java:21) -  [TestCalleeA.I2] error java.lang.NoClassDefFoundError: Could not initialize class TestCalleeA
	TestCallerA.lambda$main$8(TestCallerA.java:49)
	TestCallerA.lambda$run$2(TestCallerA.java:34)
	java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	java.lang.Thread.run(Thread.java:748)

2022-06-11 22:14:13.204 [Thread-6] INFO  TestCallerA(TestCallerA.java:35) -  success [TestCalleeA.I3]

2022-06-11 22:14:13.220 [Thread-6] ERROR TestCallerA(TestCallerA.java:21) -  [TestCalleeA.S3] error java.lang.NoClassDefFoundError: Could not initialize class TestCalleeA
	TestCallerA.lambda$main$10(TestCallerA.java:51)
	TestCallerA.lambda$run$2(TestCallerA.java:34)
	java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	java.lang.Thread.run(Thread.java:748)

2022-06-11 22:14:13.236 [Thread-7] ERROR TestCallerA(TestCallerA.java:21) -  [TestCalleeA.S3 set] error java.lang.NoClassDefFoundError: Could not initialize class TestCalleeA
	TestCallerA.lambda$main$11(TestCallerA.java:52)
	TestCallerA.lambda$run$2(TestCallerA.java:34)
	java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	java.lang.Thread.run(Thread.java:748)

2022-06-11 22:14:13.252 [Thread-8] ERROR TestCallerA(TestCallerA.java:21) -  [TestCalleeA.S4] error java.lang.NoClassDefFoundError: Could not initialize class TestCalleeA
	TestCallerA.lambda$main$12(TestCallerA.java:53)
	TestCallerA.lambda$run$2(TestCallerA.java:34)
	java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	java.lang.Thread.run(Thread.java:748)

2022-06-11 22:14:13.267 [Thread-9] INFO  TestCallerA(TestCallerA.java:35) -  success [TestCalleeA.S5]