NoClassDefFoundError与类加载

作者: lfckop | 来源:发表于2017-12-02 16:46 被阅读344次

    一句话总结

    若一个类在类加载过程中的任一阶段出错,随后对该类的任何主动引用都只会抛出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组里各位同事的帮助,在此一并感谢。

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

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

    相关文章

      网友评论

        本文标题:NoClassDefFoundError与类加载

        本文链接:https://www.haomeiwen.com/subject/fjwnbxtx.html