问题出现在一次需求临近上线,预发环境回归验证的时候,测试突然反馈端面打不开了,直接报500,第一反应是怎么可能!!!自己头铁试了一把,还真是500。。。接着一顿排查,环境是不是在重启、分支有没有被污染、release有没有问题。对比线上已发布代码,基本可以定位到是release分支其他需求的逻辑导致的。然后一头扎进日志,拿trace从入口应用开始刨,很快发现这么一错误
java.lang.NoClassDefFoundError: Could not initialize xxx.xx.xxx.xxx
第一反应这不是class文件缺失或者class冲突(给错看成ClassNotFoundException了)导致的吗,先检查class文件有没有问题,果不其然,都不是。这错报的有点莫名其妙,至少给个业务相关的提示吧,没有。那就换种思路吧,先度娘,不,先gg一下,很快有了结果,我们先来看下这位小哥遇到的问题:
ps:看了眼提问时间,居然是9年前。。。,查看次数达到了424k,惭愧+ 脸红ing。。。
[java.lang.NoClassDefFoundError: Could not initialize class XXX](https://stackoverflow.com/questions/7325579/java-lang-noclassdeffounderror-could-not-initialize-class-xxx)
public class PropHolder {
public static Properties prop;
static {
//code for loading properties from file
}
}
// Referencing the class somewhere else:
Properties prop = PropHolder.prop;
这问题跟我遇到的不是一毛一样吗,再看看下面的回复,下面是获赞最多的答案:
My best bet is there is an issue here:
static {
//code for loading properties from file
}
It would appear some uncaught exception occurred and propagated up to the actual ClassLoader attempting to load the class. We would need a stacktrace to confirm this though.
Either that or it occurred when creating `PropHolder.prop` static variable.
基本意思就是说,类加载器尝试加载类的时候,在static代码块中出现了uncaught exception。代码上看,既有可能是在执行static代码块或者创建静态变量prop的过程中出现了异常,没有进行处理。
对比了下,我遇到的问题,出现问题的class是个枚举(枚举的问题后面详述),而且没有static,我一寻思,先把这种情况记录下来,接着查,然后看到了同样的答案,而且有验证demo,于是迫不及待copy下来,验证了一把,代码如下:
TestCase.class
private static void testGeneralClass(){
System.out.println("java.lang.NoClassDefFoundError Simulator - Training 2");
System.out.println("Author: Pierre-Hugues Charbonneau");
System.out.println("http://javaeesupportpatterns.blogspot.com\n\n");
try {
// Create a new instance of ClassA (attempt #1)
System.out.println("FIRST attempt to create a new instance of ClassA...\n");
ClassA classA = new ClassA();
} catch (Throwable any) {
any.printStackTrace();
}
try {
// Create a new instance of ClassA (attempt #2)
System.out.println("\nSECOND attempt to create a new instance of ClassA...\n");
ClassA classA = new ClassA();
} catch (Throwable any) {
any.printStackTrace();
}
try {
// Create a new instance of ClassA (attempt #3)
System.out.println("\nTHIRD attempt to create a new instance of ClassA...\n");
ClassA classA = new ClassA();
} catch (Throwable any) {
any.printStackTrace();
}
System.out.println("\n\ndone!");
}
ClassA.class
public class ClassA {
private final static boolean REPLICATE_PROBLEM1 = true; // static variable initializer
private final static boolean REPLICATE_PROBLEM2 = false; // static block{} initializer
// Static variable executed at Class loading time
private static String staticVariable = initStaticVariable();
// Static initializer block executed at Class loading time
static {
// Static block code execution...
if (REPLICATE_PROBLEM2) throw new IllegalStateException("ClassA.static{}: Internal Error!");
}
public ClassA() {
System.out.println("Creating a new instance of "+ClassA.class.getName()+"...");
}
private static String initStaticVariable() {
String stringData = "";
if (REPLICATE_PROBLEM1) throw new IllegalStateException("ClassA.initStaticVariable(): Internal Error!");
return stringData;
}
}
代码逻辑比较简单,testcase里面,连续3次创建classA对象;重点关注ClassA的定义,定义一个静态变量staticVariable,初始化时抛出异常,还有一段静态代码块,同样执行的时候抛出异常;两个开关用于切换静态变量的初始化和静态代码块的执行。下面我们看下执行结果,
java.lang.ExceptionInInitializerError
at com.edu.nbu.cn.demo.problem.TestProblem.testGeneralClass(TestProblem.java:32)
at com.edu.nbu.cn.demo.problem.TestProblem.main(TestProblem.java:15)
Caused by: java.lang.IllegalStateException: ClassA.initStaticVariable(): Internal Error!
at com.edu.nbu.cn.demo.problem.ClassA.initStaticVariable(ClassA.java:36)
at com.edu.nbu.cn.demo.problem.ClassA.<clinit>(ClassA.java:15)
... 2 more
java.lang.NoClassDefFoundError: Could not initialize class com.edu.nbu.cn.demo.problem.ClassA
at com.edu.nbu.cn.demo.problem.TestProblem.testGeneralClass(TestProblem.java:41)
at com.edu.nbu.cn.demo.problem.TestProblem.main(TestProblem.java:15)
java.lang.NoClassDefFoundError: Could not initialize class com.edu.nbu.cn.demo.problem.ClassA
at com.edu.nbu.cn.demo.problem.TestProblem.testGeneralClass(TestProblem.java:50)
at com.edu.nbu.cn.demo.problem.TestProblem.main(TestProblem.java:15)
错误提示的很明显了,无法初始化类。我们来看看作者对这种情况的建议:
1、浏览NoClassDefFoundError,确认出现错误的class
2、找出受影响的类,看看是否包含有静态初始化代码(包括静态代码块、静态变量)
3、检查服务日志,查看是否有静态代码初始化导致的错误(比如:ExceptionininitializerError)
4、一旦确定,深入分析代码,并确定引起静态代码块初始化失败的根本原因。
跟随指引,我马上又去日志里搂了一遍,关键字ExceptionininitializerError,还真有,还真的是这个枚举,最后定位到是因为枚举的一个不定长参数导致的(业务对不定长参数加了长度限制,比如参数长度不少于2),好,问题基本到此解决,这仅仅是开始。
我们来思考两个问题:
1、JVM如何处理类加载过程中的错误?
2、枚举类的加载与普通类有什么区别?
先看第一个问题,类加载过程出错,JVM如何应对?回顾一下类的加载过程以及加载时机,经典三部
1、加载:class文件到JVM
2、连接:包括3小步,
验证-魔数验证、版本校验、符号引用验证(使用的类无法找到则会抛出NoClassDefFoundError,找不到方法则抛出NoSuchMethodError);
准备-为类分配空间,为所有非final字段赋默认值,static final字段则直接赋值
解析- 简而言之,就是把类、字段、方法、接口的符号引用替换成直接引用。
3、初始化:执行类的初始化方法<cinit>,<cinit>方法由编译器自动生成,内部执行静态代码块以及静态变量的初始化。
ps:这里其实就可以看出来,本次遇到的问题其实在使用枚举类的时候,使用类在验证阶段抛出的错误。
类的加载时机(前提,类没有进行过初始化)
1、遇到new、getstatic、putstatic、invokestatic这几种指令时,会触发类的初始化;
简单来说,就是在 new一个对象的时候、读取或者设置一个static字段时(final修饰字段以及常量池字段除外)、调用静态方法的时候
2、使用java.lang.reflect包的方法对类进行反射调用的时候
3、初始化类,父类没有进行初始化,则先初始化父类
4、JVM启动,用户需要指定一个主类(包含main方法)
5、当使用JDK的动态语言支持时,如果一个`java.lang.invoke.MethodHandle`实例最后的解析结果 `REF_getStatic`、`REF_putStatic`、`REF_invokeStatic`的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化
JVM规范对类加载过程中的错误的定义
1、加载阶段:
a) 如果加载阶段出错,则会抛出**LinkageError**错误,具体错误类型是**LinkageError**的子类;
b) 如果加载的不是一个**ClassFile**(非JVM指定的class文件格式),则抛出**ClassFormatError**
c) 如果版本不在有效范围之内,则抛**UnsupportedClassVersionError**
d) 如果C是一个接口,那么其直系父类(direct supperclass)必须Object,否则抛出**IncompatibleClassChangeError**
e) 如果C的直系父类是C,那么会抛出**ClassCircularityError**
f) 如果尝试加载一个处在验证(**during verification**)或者解(**during resolution**),且不在初始化阶段(**not initialization**),这个时候类加载器会抛出**ClassNotFoundException**,而JVM则抛出因为ClassNotFoundException导致的**NoClassDefFoundError**。
2、连接阶段:
注意: 类或者接口中的符号引用解析不一定会在连接阶段进行。由于连接阶段涉及空间分配,可能会抛出OutOfMemoryError
a、验证 - 保证class文件在结构上的正确性;同时,验证阶段可能会导致其他类的加载,但无需对这些类进行验证或准备
如果未通过验证,则会抛出**VerifyError**;
如果JVM在尝试验证类或者接口,因为**LinkageError**(或者子类)错误抛出,那么,后续的验证同样也会抛出**LinkageError**错误。
b、准备 - 创建类或者接口的静态域,并初始化为默认值。注意:这里不会执行任何代码逻辑;且准备阶段必须在初始化开始之前结束。
c、解析-JVM执行以下指令时,需要进行符号引用的解析;
*anewarray*(创建新数组), *checkcast*(类型检查), *getfield*, *getstatic*, *instanceof*, *invokedynamic*, *invokeinterface*, *invokespecial*, *invokestatic*, *invokevirtual*, *ldc*, *ldc_w*, *multianewarray*, *new*, *putfield*, and *putstatic* make symbolic references to the run-time constant pool.
这里有几个点需要注意:
1、解析其实就是动态决定运行时常量池中符号引用具体值的过程。
2、一次*invokedynamic*指令的符号引用解析并不意味着该符号引用对于其他*invokedynamic*指令来讲已解析。
3、具体*invokedynamic*指令解析的实际值与具体*invokedynamic*一一对应
4、已解析的符号引用,再次解析时会返回与之前一样的实体解析阶段可能抛出的错误:**IncompatibleClassChangeError**或者其子类对象;如果符号引用解析失败是因为抛出了**LinkageError**,那么会直接将**LinkageError**抛出。
(1)、类、接口解析 :如果C不可访问,那么会抛出**IllegalAccessError**
(2)、字段解析:
如果解析失败则抛出**NoSuchFieldError**
如果解析成功,但是不可访问,则抛出**NoSuchFieldError**
(3)、方法解析
如果C是一个接口,那么会抛出IncompatibleClassChangeError
方法查找失败,则抛出**NoSuchMethodError**
方法查找成功,但是不可访问,则抛出**IllegalAccessError**
注意:父类的private、static方法,不会被解析;已解析的方法所控制的行为,不依赖于该方法是否抽象
(4)、接口方法解析与方法解析类似
(5)、Method Type and Method Handle解析
3、初始化
在初始化阶段,类或者接口必须是已连接状态,即 已验证 + 已准备完毕 + 可能已解析;JVM的退出,可以调用Runtime的exit或者halt方法,也可以调用System的exit方法。