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]