java类static初始化代码块中抛出未预期的异常,导致该类无法被正常加载

    最近在项目中发现一个比较有意思的异常情况,特此记录一下。

    某个类在static初始化代码块中启动了一个线程,用于监控该类的运行情况并打印日志,代码示意如下。 

public class Test {
  
   static {
      new Thread(new Runnable(){}).start();
   }
   ...// other code
}

在项目运行过程中,有某一处需要用到此类,加载该类时,恰好机器在这个时间点内存不够用了,导致线程启动失败,抛出了一个异常:java.lang.OutOfMemoryError:unable to create new native thread。这个异常很好理解,但是之后的日志中发现一直在打印着同一个异常:

java.lang.NoClassDefFoundError:Could not initialize class xx.xx.Test(上面定义的那个Test类)

根据上下文可以猜测就是因为在static块中创建线程的时候抛出的OutOfMemoryError导致了这个Test类再也无法被加载了,从而导致了后续的一系列NoClassDefFoundError。为了验证这个猜想,自己写了段代码测试

import java.util.Date;

import org.apache.commons.lang3.time.DateUtils;

/**
 * @author klein.lei
 * @version $Id: TestStaticError.java, v 0.1 2018年02月27日 下午8:28 klein.lei Exp $
 */
public class TestStaticError {

    static {
        if (new Date().after(DateUtils.addDays(new Date(), -1))) { // 这里是为了保证一定抛出该异常,如果你在static块中直接抛异常,编译无法通过
            throw new RuntimeException();
        }
    }

    public void test1() {
        System.out.println("hello world");
    }
}

main方法

import java.util.Scanner;

/**
 * @author klein.lei
 * @version $Id: Test.java, v 0.1 2018年02月27日 下午8:28 klein.lei Exp $
 */
public class Test {

    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                TestStaticError testStaticError = new TestStaticError();
                testStaticError.test1();
            }
        }).start();
        Thread.sleep(200L);
        for (int i = 0; i < 10; i ++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    TestStaticError testStaticError = new TestStaticError();
                    testStaticError.test1();
                }
            }).start();
        }
        new Scanner(System.in).next();
    }
}

运行可以得到以下结果

Exception in thread "Thread-0" java.lang.ExceptionInInitializerError
	at learning.web.Test$1.run(Test.java:19)
	at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.RuntimeException
	at learning.web.TestStaticError.<clinit>(TestStaticError.java:19)
	... 2 more
Exception in thread "Thread-1" Exception in thread "Thread-2" java.lang.NoClassDefFoundError: Could not initialize class learning.web.TestStaticError
	at learning.web.Test$2.run(Test.java:28)
	at java.lang.Thread.run(Thread.java:745)
Exception in thread "Thread-4" Exception in thread "Thread-3" java.lang.NoClassDefFoundError: Could not initialize class learning.web.TestStaticError
	at learning.web.Test$2.run(Test.java:28)
	at java.lang.Thread.run(Thread.java:745)
Exception in thread "Thread-6" java.lang.NoClassDefFoundError: Could not initialize class learning.web.TestStaticError
	at learning.web.Test$2.run(Test.java:28)
	at java.lang.Thread.run(Thread.java:745)
java.lang.NoClassDefFoundError: Could not initialize class learning.web.TestStaticError
	at learning.web.Test$2.run(Test.java:28)
	at java.lang.Thread.run(Thread.java:745)
Exception in thread "Thread-10" Exception in thread "Thread-5" java.lang.NoClassDefFoundError: Could not initialize class learning.web.TestStaticError
	at learning.web.Test$2.run(Test.java:28)
	at java.lang.Thread.run(Thread.java:745)
Exception in thread "Thread-9" Exception in thread "Thread-8" java.lang.NoClassDefFoundError: Could not initialize class learning.web.TestStaticError
	at learning.web.Test$2.run(Test.java:28)
	at java.lang.Thread.run(Thread.java:745)
Exception in thread "Thread-7" java.lang.NoClassDefFoundError: Could not initialize class learning.web.TestStaticError
	at learning.web.Test$2.run(Test.java:28)
	at java.lang.Thread.run(Thread.java:745)
java.lang.NoClassDefFoundError: Could not initialize class learning.web.TestStaticError
	at learning.web.Test$2.run(Test.java:28)
	at java.lang.Thread.run(Thread.java:745)
java.lang.NoClassDefFoundError: Could not initialize class learning.web.TestStaticError
	at learning.web.Test$2.run(Test.java:28)
	at java.lang.Thread.run(Thread.java:745)
java.lang.NoClassDefFoundError: Could not initialize class learning.web.TestStaticError
	at learning.web.Test$2.run(Test.java:28)
	at java.lang.Thread.run(Thread.java:745)

可以发现第一次是抛出了一个runtimeException,之后都是java.lang.NoClassDefFoundError,表明上面的猜想是正确,至于为什么会出现这个情况,可能需要翻一番ClassLoader的源码了。

google一番后找到了答案,stackoverflow上有人提过这个问题了(点击打开链接)(果然程序员需要掌握面向stackoverflow编程)

简单总结一下:

我们知道java中的每一个类C都有一个相应的Class对象(java.lang.Class),这个Class对象用于描述类C的一些信息,包括类C的状态,类C的Class对象描述了类C的4种状态。

1.类C已经通过验证(verified)和准备(prepared)的阶段,但仍未初始化。

(注:类的加载过程)


2.类C正在被其他线程初始化中。

3.类C已经被其他线程初始化完成。

4.类C处于一个错误的状态(erroneous state),可能是由于尝试初始化的时候失败了。

根据oracle提供的文档(点击打开链接),jvm初始化一个类时必须严格遵守以下规范:

1.对于每一个类C来说,都有一个唯一的初始化锁LC,初始化一个类时,当前线程必须先获取这个锁。

2.根据类C的Class对象的状态指示,如果类C正在被其他线程初始化,则释放当前锁LC,并且阻塞当前线程直到类初始化完成。

3.根据类C的Class对象的状态指示,如果类C正在被当前线程初始化,则说明是递归调用,需要释放锁LC,并正常结束请求。

4.根据类C的Class对象的状态指示,如果类C已经初始化完成,需要释放锁LC,并正常结束请求。

5.根据类C的Class对象的状态指示,如果类C处于erroneous state,那么表示类的初始化无法完成,此时需要释放锁同时抛出一个NoClassDefFoundError

6.如果不是2、3、4、5中的情况,说明当前线程可以正常初始化类,此时需要在C的Class对象中记录当前线程正在初始化该类C,然后释放锁LC,然后会初始化类C中的final字段和其他在编译期间就可以确定的常量字段。

7.如果类C不是接口,且C的父类SC还没有被初始化,则再针对SC重复初始化类的所有步骤,有必要的话,先对SC进行验证和准备。如果SC的初始化过程中抛出了一个异常,那么这个初始化过程将会被意外中止,然后需要获取锁LC,在类C的Class对象中标记类C为erroneous state,标记完成后,唤醒其他等待的线程,释放锁LC,最后抛出和SC初始化中过程抛出的同样的异常,中止类C的初始化过程。

8.接下来,判断类C的assertion(点击打开链接)是否生效。

9.接下来,按照代码文本定义的顺序(注:

public class {
   int a = getA();
   int b = getB();
}

文本顺序在这里指的就是先初始化a,再初始化b),对类的字段、static块代码、和接口上定义的字段进行初始化.

10.如果这个执行过程正常结束,则获取锁LC,标记类C的Class对象已经完全初始化,唤醒其他所有线程,释放锁,正常结束流程

11.如果这个执行过程出现异常,抛出了一个异常E导致过程被意外中止,如果这个异常不是是Error或者是Error的子类,则创建一个ExceptionInInitializerError,将E作为ExceptionInInitializerError的构造函数参数,在接下来的步骤中用ExceptionInInitializerError替换E,如果因为出现了OutOfMemoryError错误而导致无法创建ExceptionInInitializerError对象,则在接下来的步骤中用OutOfMemoryError替换E。

12.尝试获取锁LC,标记类C的Class对象为erroneous state,唤起所有等待的线程,释放锁LC,用步骤11中出现的异常E(或则是ExceptionInInitializerError或OutOfMemoryError)中止整个流程。

根据以上步骤我们可以发现,static块的代码初始化是在步骤9中初始化,如果这时候抛出了一个异常,非Error,则抛出了ExceptionInInitializerError,符合我们的测试代码,因为类的初始化过程都是加锁的,当第一个线程加载类的时候抛出了异常,此时类的状态被标记erroneous state,当其他线程再来加载该类的时候,直接进入步骤5所描述的过程,抛出NoClassDefFoundError,同样符合我们的测试代码表现。

    至此,整个异常出现的原因也非常明了了,在代码编写过程中,我们应该尽量避免在static块代码或者类的成员变量中抛出异常,因为类的初始化过程无法重试,一旦在初始化过程中出现异常,那么这个类在之后都无法被正常加载了。

  • 8
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值