一句话总结
若一个类在类加载过程中的任一阶段出错,随后对该类的任何主动引用都只会抛出java.lang.NoClassDefFoundError
异常,这时需要在日志中逆流而上找出其原始异常(Root Cause),才能确定是在类加载过程的哪一阶段出错和具体出错原因。原始异常通常就在NoClassDefFoundError
第一次出现的地方的前面。
java.lang.NoClassDefFoundError
该异常在API文档中的描述为:
public class NoClassDefFoundError extends LinkageError
Thrown if the Java Virtual Machine or a ClassLoader instance tries to load in the definition of a class (as part of a normal method call or as part of creating a new instance using the new expression) and no definition of the class could be found.
The searched-for class definition existed when the currently executing class was compiled, but the definition can no longer be found.
类加载的几个阶段和分别对应的方法
类加载(Class Loading)过程中的几个阶段包括:加载(Loading)、链接(Linking)、初始化(Initialization),其中链接阶段又可细分为验证(Verification)、准备(Preparation)、解析(Resolution)三个过程。各个阶段的具体描述请参考周志明或The Java Virtual Machine Specification - Loading, Linking, and Initializing,它们分别对应的主要方法简介如下。在遇到类加载相关问题时,可以用BTrace的@OnMethod
注解来跟踪方法的执行。
-
加载
protected final Class<?> defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain)
It converts an array of bytes into an instance of class
Class
.位于
java.lang.ClassLoader
中。 -
链接
protected final void resolveClass(Class<?> c)
Links the specified class. This (misleadingly named) method may be used by a class loader to link a class.
位于
java.lang.ClassLoader
中。 -
初始化
<clinit>()
"the class or interface initialization method"
,类加载过程的最后一步,执行类构造器方法,它是编译器自动收集类中的所有类变量(static
)的赋值动作和静态语句块(static {}
)中的语句合并产生的。
JVM记录类加载状态
不甚懂C++,粗略地翻了下源码。
从OpenJDK/jdk8/jdk8/hotspot的源码可以看出虚拟机内部有个类InstanceKlass
,它有一个_init_state
变量记录Java类的类加载状态,摘录如下:
hotspot-87ee5ee27509/src/share/vm/oops/instanceKlass.hpp:
An InstanceKlass is the VM level representation of a Java class. It contains all information needed for a class at execution runtime.
enum ClassState {
allocated, // allocated (but not yet linked)
loaded, // loaded and inserted in class hierarchy (but not linked yet)
linked, // successfully linked/verified (but not initialized yet)
being_initialized, // currently running class initializer
fully_initialized, // initialized (successfull final state)
initialization_error // error happened during initialization
};
u1 _init_state; // state of class
// initialization state
bool is_loaded() const { return _init_state >= loaded; }
bool is_linked() const { return _init_state >= linked; }
bool is_initialized() const { return _init_state == fully_initialized; }
bool is_not_initialized() const { return _init_state < being_initialized; }
bool is_being_initialized() const { return _init_state == being_initialized; }
bool is_in_error_state() const { return _init_state == initialization_error; }
hotspot-87ee5ee27509/src/share/vm/oops/instanceKlass.cpp:
关注类加载的最后一步:初始化,它由6种主动引用来触发(A class or interface C may be initialized only as a result of)。类初始化主要由InstanceKlass::initialize_impl
函数来完成,它基本按照JVM Specification - Initialization中描述的1 - 12
步来执行,摘录部分如下:
void InstanceKlass::initialize_impl(instanceKlassHandle this_oop, TRAPS) {
// Make sure klass is linked (verified) before initialization
// A class could already be verified, since it has been reflected upon.
this_oop->link_class(CHECK);
// Step 5
if (this_oop->is_in_error_state()) {
DTRACE_CLASSINIT_PROBE_WAIT(erroneous, InstanceKlass::cast(this_oop()), -1,wait);
ResourceMark rm(THREAD);
const char* desc = "Could not initialize class ";
const char* className = this_oop->external_name();
size_t msglen = strlen(desc) + strlen(className) + 1;
char* message = NEW_RESOURCE_ARRAY(char, msglen);
if (NULL == message) {
// Out of memory: can't create detailed error message
THROW_MSG(vmSymbols::java_lang_NoClassDefFoundError(), className);
} else {
jio_snprintf(message, msglen, "%s%s", desc, className);
THROW_MSG(vmSymbols::java_lang_NoClassDefFoundError(), message);
}
}
// Step 8
{
assert(THREAD->is_Java_thread(), "non-JavaThread in initialize_impl");
JavaThread* jt = (JavaThread*)THREAD;
DTRACE_CLASSINIT_PROBE_WAIT(clinit, InstanceKlass::cast(this_oop()), -1,wait);
// Timer includes any side effects of class initialization (resolution,
// etc), but not recursive entry into call_class_initializer().
PerfClassTraceTime timer(ClassLoader::perf_class_init_time(),
ClassLoader::perf_class_init_selftime(),
ClassLoader::perf_classes_inited(),
jt->get_thread_stat()->perf_recursion_counts_addr(),
jt->get_thread_stat()->perf_timers_addr(),
PerfClassTraceTime::CLASS_CLINIT);
this_oop->call_class_initializer(THREAD);
}
可见,类执行初始化之前会先检查确保类已经执行过链接过程。
同样,代码"step 5"
表明,若抛出异常"java.lang.NoClassDefFoundError: Could not initialize class xxx.package.ClassName"
(尤其注意"Could not initialize class"),则表明一定是执行类构造器<clinit>()
时出错。
当你见到这个异常描述信息时,说明其实是已经执行完<clinit>()
了,这时应该找出第一次抛异常的地方!因为类第一次初始化失败后,后续使用此类时不会再次初始化,只会抛出以上信息,此时再用BTrace来跟踪其<clinit>()
也是徒劳的。用Btrace来启动项目是更好的选择,或者在项目刚刚启动时就进行跟踪。
5. If the Class object for C is in an erroneous state, then initialization is not possible. Release LC and throw a NoClassDefFoundError.
代码"step 8"
执行类构造器<clinit>()
,JVM表现为调用this_oop->call_class_initializer(THREAD)
函数。如果类构造器执行成功,则置类加载状态为成功,即:this_oop->set_initialization_state_and_notify(fully_initialized, CHECK);
。
如果类构造器执行失败(记抛出的异常为E
),则置类加载状态为初始化错误,即:this_oop->set_initialization_state_and_notify(initialization_error, THREAD);
;若E
不是Error
类或其子类,则用E
构造一个ExceptionInInitializerError
,并抛出;这种情形下,当以后再有对该类的主动引用触发类初始化时,则直接走到"step 5"
并抛出异常"java.lang.NoClassDefFoundError: Could not initialize class xxx.package.ClassName"
。
案例
案例1
有一个spring-boot的项目,包含service、api、web三个子工程,分别被打包成可运行jar并以java -jar
的方式来启动,它们在测试环境中部署在一台机器上。当web工程频繁更新时,会重新打包整个工程并重新启动web,这样service、api的目标可运行jar包会在运行中被改变。然后,service、api工程会频繁抛出这样的异常:java.lang.NoClassDefFoundError: xxx/package/ClassName1
,在日志中其原始异常基本是这样的:
(java.util.zip.ZipException: invalid code lengths set)
(java.util.zip.ZipException: invalid distance too far back)
java.util.zip.ZipException: invalid block type
Wrapped by: java.lang.ClassNotFoundException: xxx.package.ClassName1
Wrapped by: java.lang.NoClassDefFoundError: xxx/package/ClassName1
Wrapped by: org.springframework.web.util.NestedServletException: Handler processing failed; nested exception is java.lang.NoClassDefFoundError: xxx/package/ClassName1
...
其原始异常java.util.zip.ZipException
的错误描述有好几种,如示列在异常栈最上面的()
中。
为什么重新打包会导致ZipException
并导致NoClassDefFoundError
类加载错误呢?这与spring-boot的类加载策略有关。对boot这种可运行jar(jar in jar,又叫nested jar,或fat jar),spring-boot会首先计算各个类文件在jar中的位置索引,当需要加载某个类时,就依照位置索引去读二进流解压进行类加载;然而当目标jar被重新打包后,之前计算出的位置索引就不准确了,导致后续的类加载报java.util.zip.ZipException
异常,并导致类加载失败。参考spring开发者philwebb在该Issue下的回答。
案例2
某工程的某个类由于执行类构造器<clinit>()
失败而导致类加载失败。其原始异常大致如下:
严重: Servlet.service() for servlet [appServlet] in context with path [/saas-ms] threw exception [Handler processing failed; nested exception is java.lang.ExceptionInInitializerError] with root cause
java.util.MissingResourceException: Can't find bundle for base name messages, locale zh_CN
at java.util.ResourceBundle.throwMissingResourceException(ResourceBundle.java:1564)
at java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:1387)
at java.util.ResourceBundle.getBundle(ResourceBundle.java:845)
at com.letvcloud.saas.platform.util.ResourceUtils.getValue(ResourceUtils.java:14)
at com.letvcloud.saas.platform.enums.DaysUnit.<clinit>(DaysUnit.java:14)
...
类加载失败后,后续对该类的调用都抛出NoClassDefFoundError
异常:
严重: Servlet.service() for servlet [appServlet] in context with path [/saas-ms] threw exception [Handler processing failed; nested exception is java.lang.NoClassDefFoundError: Could not initialize class com.letvcloud.saas.platform.enums.DaysUnit] with root cause
java.lang.NoClassDefFoundError: Could not initialize class com.letvcloud.saas.platform.enums.DaysUnit
at com.letv.saas.ms.controller.InviteCodeController.inviteCodeApply(InviteCodeController.java:91)
...
后记
本文源自在项目中排查类加载相关问题的总结,排查过程中离不开SaaS组里各位同事的帮助,在此一并感谢。
文章如有错误或描述不当之处,请务必指出,非常感谢。
转载请务必注明文章出处。