NoClassDefFoundError与类加载

一句话总结

若一个类在类加载过程中的任一阶段出错,随后对该类的任何主动引用都只会抛出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注解来跟踪方法的执行。

  1. 加载

    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中。

  2. 链接

    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中。

  3. 初始化

    <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组里各位同事的帮助,在此一并感谢。

文章如有错误或描述不当之处,请务必指出,非常感谢。

转载请务必注明文章出处。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容