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

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

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."":()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的方法中,有对以下静态变量进行赋值:

S1

S2

I2

S3

S4

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

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

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

I1

I3

S5

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

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

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

被调用类中被访问的方法、变量调用类访问时使用的指令指令的说明调用TestCalleeA.empty()静态方法invokestatic #46Method TestCalleeA.empty:()V读取TestCalleeA.S1静态变量getstatic #45Field TestCalleeA.S1:Ljava/lang/String;读取TestCalleeA.S2静态变量getstatic #44Field TestCalleeA.S2:Ljava/lang/String;读取TestCalleeA.I1静态变量sipush 1111读取TestCalleeA.I2静态变量getstatic #43Field TestCalleeA.I2:I读取TestCalleeA.I3静态变量sipush 3333读取TestCalleeA.S3静态变量getstatic #41Field TestCalleeA.S3:Ljava/lang/String;修改TestCalleeA.S3静态变量putstatic #41Field TestCalleeA.S3:Ljava/lang/String;读取TestCalleeA.S4静态变量getstatic #39Field TestCalleeA.S4:Ljava/lang/String;读取TestCalleeA.S5静态变量ldc #37String S5_value

4.5. ExceptionInInitializerError、NoClassDefFoundError出现原因

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

4.5.1. Java Virtual Machine Specification中的说明

参考“The Java® Virtual Machine Specification Java SE 8 Edition”,“5.5. Initialization”https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.5,关于类的初始化过程的说明。

在调用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类进行初始化时,会调用其方法;

在TestCalleeA类的方法中,对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.(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.(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.(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]

文章链接

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: