不可逆的类初始化过程

简介: 不可逆的类初始化过程

类的加载过程说复杂很复杂,说简单也简单,说复杂是因为细节很多,比如说今天要说的这个,可能很多人都不了解;说简单,大致都知道类加载有这么几个阶段,loaded->linked->initialized。


本文我想说的是最后一个阶段,类的初始化,但是也不细说其中的过程,只围绕我们今天要说的展开。


我们定义一个类的时候,可能有静态变量,可能有静态代码块,这些逻辑编译之后会封装到一个叫做clinit的方法里,比如下面的代码:


class BadClass{
    private static int a=100;
    static{
        System.out.println("before init");
        int b=3/0;
        System.out.println("after init");
    }
    public static void doSomething(){
        System.out.println("do somthing");
    }
}

编译之后我们通过javap -verbose BadClass可以看到如下字节码:


{
  BadClass();
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0
  public static void doSomething();
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String do somthing
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 10: 0
        line 11: 8
  static {};
    flags: ACC_STATIC
    Code:
      stack=2, locals=1, args_size=0
         0: bipush        100
         2: putstatic     #5                  // Field a:I
         5: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc           #6                  // String before init
        10: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        13: iconst_3
        14: iconst_0
        15: idiv
        16: istore_0
        17: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        20: ldc           #7                  // String after init
        22: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        25: return
      LineNumberTable:
        line 2: 0
        line 4: 5
        line 5: 13
        line 6: 17
        line 7: 25
}

我们看到最后那个方法static{},其实就是我上面说的clinit方法,我们看到静态字段的初始化和静态代码库都封装在这个方法里。


假如我们通过如下代码来测试上面的类:


public static void main(String args[]){
        try{
            BadClass.doSomething();
        }catch (Throwable e){
            e.printStackTrace();
        }
        BadClass.doSomething();
    }

大家觉得输出会是什么?是会打印多次before init吗?其实不然,输出结果如下:


before init
java.lang.ExceptionInInitializerError
    at ObjectTest.main(ObjectTest.java:7)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)
Caused by: java.lang.ArithmeticException: / by zero
    at BadClass.<clinit>(ObjectTest.java:25)
    ... 6 more
Exception in thread "main" java.lang.NoClassDefFoundError: Could not initialize class BadClass
    at ObjectTest.main(ObjectTest.java:12)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)

也就是说其实是只输出了一次before init,这是为什么呢?


clinit方法在我们第一次主动使用这个类的时候会触发执行,比如我们访问这个类的静态方法或者静态字段就会触发执行clinit,但是这个过程是不可逆的,也就是说当我们执行一遍之后再也不会执行了,如果在执行这个方法过程中出现了异常没有被捕获,那这个类将永远不可用,虽然我们上面执行BadClass.doSomething()的时候catch住了异常,但是当代码跑到这里的时候,在jvm里已经将这个类打上标记了,说这个类初始化失败了,下次再初始化的时候就会直接返回并抛出类似的异常java.lang.NoClassDefFoundError: Could not initialize class BadClass,而不去再次执行初始化的逻辑,具体可以看下jvm里对类的状态定义:


enum ClassState {
    unparsable_by_gc = 0,               // object is not yet parsable by gc. Value of _init_state at object allocation.
    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
  };


如果clinit执行失败了,抛了一个未被捕获的异常,那将这个类的状态设置为initialization_error,并且无法再恢复,因为jvm会认为你这次初始化失败了,下次肯定也是失败的,为了防止不断抛这种异常,所以做了一个缓存处理,不是每次都再去执行clinit,因此大家要特别注意,类的初始化过程可千万不能出错,出错就可能只能重启了哦。类的加载过程说复杂很复杂,说简单也简单,说复杂是因为细节很多,比如说今天要说的这个,可能很多人都不了解;说简单,大致都知道类加载有这么几个阶段,loaded->linked->initialized。


本文我想说的是最后一个阶段,类的初始化,但是也不细说其中的过程,只围绕我们今天要说的展开。


我们定义一个类的时候,可能有静态变量,可能有静态代码块,这些逻辑编译之后会封装到一个叫做clinit的方法里,比如下面的代码:


class BadClass{
    private static int a=100;
    static{
        System.out.println("before init");
        int b=3/0;
        System.out.println("after init");
    }
    public static void doSomething(){
        System.out.println("do somthing");
    }
}

编译之后我们通过javap -verbose BadClass可以看到如下字节码:


{
  BadClass();
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0
  public static void doSomething();
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String do somthing
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 10: 0
        line 11: 8
  static {};
    flags: ACC_STATIC
    Code:
      stack=2, locals=1, args_size=0
         0: bipush        100
         2: putstatic     #5                  // Field a:I
         5: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc           #6                  // String before init
        10: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        13: iconst_3
        14: iconst_0
        15: idiv
        16: istore_0
        17: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        20: ldc           #7                  // String after init
        22: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        25: return
      LineNumberTable:
        line 2: 0
        line 4: 5
        line 5: 13
        line 6: 17
        line 7: 25
}

我们看到最后那个方法static{},其实就是我上面说的clinit方法,我们看到静态字段的初始化和静态代码库都封装在这个方法里。


假如我们通过如下代码来测试上面的类:


public static void main(String args[]){
        try{
            BadClass.doSomething();
        }catch (Throwable e){
            e.printStackTrace();
        }
        BadClass.doSomething();
    }

大家觉得输出会是什么?是会打印多次before init吗?其实不然,输出结果如下:


before init
java.lang.ExceptionInInitializerError
    at ObjectTest.main(ObjectTest.java:7)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)
Caused by: java.lang.ArithmeticException: / by zero
    at BadClass.<clinit>(ObjectTest.java:25)
    ... 6 more
Exception in thread "main" java.lang.NoClassDefFoundError: Could not initialize class BadClass
    at ObjectTest.main(ObjectTest.java:12)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)

也就是说其实是只输出了一次before init,这是为什么呢?


clinit方法在我们第一次主动使用这个类的时候会触发执行,比如我们访问这个类的静态方法或者静态字段就会触发执行clinit,但是这个过程是不可逆的,也就是说当我们执行一遍之后再也不会执行了,如果在执行这个方法过程中出现了异常没有被捕获,那这个类将永远不可用,虽然我们上面执行BadClass.doSomething()的时候catch住了异常,但是当代码跑到这里的时候,在jvm里已经将这个类打上标记了,说这个类初始化失败了,下次再初始化的时候就会直接返回并抛出类似的异常java.lang.NoClassDefFoundError: Could not initialize class BadClass,而不去再次执行初始化的逻辑,具体可以看下jvm里对类的状态定义:


enum ClassState {
    unparsable_by_gc = 0,               // object is not yet parsable by gc. Value of _init_state at object allocation.
    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
  };


如果clinit执行失败了,抛了一个未被捕获的异常,那将这个类的状态设置为initialization_error,并且无法再恢复,因为jvm会认为你这次初始化失败了,下次肯定也是失败的,为了防止不断抛这种异常,所以做了一个缓存处理,不是每次都再去执行clinit,因此大家要特别注意,类的初始化过程可千万不能出错,出错就可能只能重启了哦。

相关文章
|
6月前
|
索引
常见任务中的双重while循环的结构
常见任务中的双重while循环的结构
|
2天前
|
算法 调度
多级反馈队列算法的具体实现过程是怎样的?
【10月更文挑战第25天】多级反馈队列算法通过动态调整进程的优先级和在不同优先级队列之间的转移,能够较好地适应不同类型进程的需求,兼顾了短作业优先、I/O密集型作业优先等多种调度策略的优点,提高了系统的整体性能和资源利用率,同时也能保证对实时性要求较高的进程能够及时得到响应。
18 4
|
5月前
new 一个对象的过程中发生了什么
new 一个对象的过程中发生了什么
|
6月前
|
Java 编译器 容器
浅谈类的加载过程
浅谈类的加载过程
27 3
|
6月前
|
Java 程序员
揭秘编程世界的构造块:一文教你理解方法的本质与运用
揭秘编程世界的构造块:一文教你理解方法的本质与运用
34 0
|
6月前
|
编译器 C++
c++拷贝对象时的优化问题
博主是基于VS2019测试的,不同编译器可能情况不一样。 看下面这一个类A:
45 0
|
安全 搜索推荐
如何避免写重复代码?两种常见的方法:抽象和组合
如何避免写重复代码?两种常见的方法:抽象和组合
241 0
|
JSON API 数据格式
利用注解 + 反射消除重复代码,妙
利用注解 + 反射消除重复代码,妙
101 0
|
存储 缓存 安全
类的加载机制以及类、对象初始化的详细过程
java类的生命周期包括加载、连接(验证、准备、解析)、初始化、使用、卸载五个阶段。初始化的顺序是怎样的呢?
145 0
类的加载机制以及类、对象初始化的详细过程
|
存储 Java C++
类加载过程和创建对象做了哪些步骤?
类加载过程和创建对象做了哪些步骤?
109 0
类加载过程和创建对象做了哪些步骤?