类的加载过程说复杂很复杂,说简单也简单,说复杂是因为细节很多,比如说今天要说的这个,可能很多人都不了解;说简单,大致都知道类加载有这么几个阶段,loaded->linked->initialized,为了让大家能更轻松地知道我今天说的这个话题,我不详细说类加载的整个过程,改天有时间有精力了我将整个类加载的过程和大家好好说说(PS:我对类加载过程慢慢清晰起来得益于当初在支付宝做cloudengine容器开发的时候,当时引入了标准的osgi,解决类加载的问题几乎是每天的家常便饭,相信大家如果还在使用OSGI,那估计能体会我当时的那种痛,哈哈)。

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

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

  1. class BadClass{
  2. private static int a=100;
  3. static{
  4. System.out.println("before init");
  5. int b=3/0;
  6. System.out.println("after init");
  7. }
  8.  
  9. public static void doSomething(){
  10. System.out.println("do somthing");
  11. }
  12. }

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

  1. {
  2. BadClass();
  3. flags:
  4. Code:
  5. stack=1, locals=1, args_size=1
  6. 0: aload_0
  7. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  8. 4: return
  9. LineNumberTable:
  10. line 1: 0
  11.  
  12. public static void doSomething();
  13. flags: ACC_PUBLIC, ACC_STATIC
  14. Code:
  15. stack=2, locals=0, args_size=0
  16. 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
  17. 3: ldc #3 // String do somthing
  18. 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  19. 8: return
  20. LineNumberTable:
  21. line 10: 0
  22. line 11: 8
  23.  
  24. static {};
  25. flags: ACC_STATIC
  26. Code:
  27. stack=2, locals=1, args_size=0
  28. 0: bipush 100
  29. 2: putstatic #5 // Field a:I
  30. 5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
  31. 8: ldc #6 // String before init
  32. 10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  33. 13: iconst_3
  34. 14: iconst_0
  35. 15: idiv
  36. 16: istore_0
  37. 17: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
  38. 20: ldc #7 // String after init
  39. 22: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  40. 25: return
  41. LineNumberTable:
  42. line 2: 0
  43. line 4: 5
  44. line 5: 13
  45. line 6: 17
  46. line 7: 25
  47. }

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

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

  1. public static void main(String args[]){
  2. try{
  3. BadClass.doSomething();
  4. }catch (Throwable e){
  5. e.printStackTrace();
  6. }
  7.  
  8. BadClass.doSomething();
  9. }

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

  1. before init
  2. java.lang.ExceptionInInitializerError
  3. at ObjectTest.main(ObjectTest.java:7)
  4. at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  5. at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
  6. at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
  7. at java.lang.reflect.Method.invoke(Method.java:606)
  8. at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)
  9. Caused by: java.lang.ArithmeticException: / by zero
  10. at BadClass.<clinit>(ObjectTest.java:25)
  11. ... 6 more
  12. Exception in thread "main" java.lang.NoClassDefFoundError: Could not initialize class BadClass
  13. at ObjectTest.main(ObjectTest.java:12)
  14. at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  15. at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
  16. at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
  17. at java.lang.reflect.Method.invoke(Method.java:606)
  18. 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里对类的状态定义:

  1. enum ClassState {
  2. unparsable_by_gc = 0, // object is not yet parsable by gc. Value of _init_state at object allocation.
  3. allocated, // allocated (but not yet linked)
  4. loaded, // loaded and inserted in class hierarchy (but not linked yet)
  5. linked, // successfully linked/verified (but not initialized yet)
  6. being_initialized, // currently running class initializer
  7. fully_initialized, // initialized (successfull final state)
  8. initialization_error // error happened during initialization
  9. };

如果clinit执行失败了,抛了一个未被捕获的异常,那将这个类的状态设置为initialization_error,并且无法再恢复,因为jvm会认为你这次初始化失败了,下次肯定也是失败的,为了防止不断抛这种异常,所以做了一个缓存处理,不是每次都再去执行clinit,因此大家要特别注意,类的初始化过程可千万不能出错,出错就可能只能重启了哦。类的加载过程说复杂很复杂,说简单也简单,说复杂是因为细节很多,比如说今天要说的这个,可能很多人都不了解;说简单,大致都知道类加载有这么几个阶段,loaded->linked->initialized,为了让大家能更轻松地知道我今天说的这个话题,我不详细说类加载的整个过程,改天有时间有精力了我将整个类加载的过程和大家好好说说(PS:我对类加载过程慢慢清晰起来得益于当初在支付宝做cloudengine容器开发的时候,当时引入了标准的osgi,解决类加载的问题几乎是每天的家常便饭,相信大家如果还在使用OSGI,那估计能体会我当时的那种痛,哈哈)。

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

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

  1. class BadClass{
  2. private static int a=100;
  3. static{
  4. System.out.println("before init");
  5. int b=3/0;
  6. System.out.println("after init");
  7. }
  8.  
  9. public static void doSomething(){
  10. System.out.println("do somthing");
  11. }
  12. }

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

  1. {
  2. BadClass();
  3. flags:
  4. Code:
  5. stack=1, locals=1, args_size=1
  6. 0: aload_0
  7. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  8. 4: return
  9. LineNumberTable:
  10. line 1: 0
  11.  
  12. public static void doSomething();
  13. flags: ACC_PUBLIC, ACC_STATIC
  14. Code:
  15. stack=2, locals=0, args_size=0
  16. 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
  17. 3: ldc #3 // String do somthing
  18. 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  19. 8: return
  20. LineNumberTable:
  21. line 10: 0
  22. line 11: 8
  23.  
  24. static {};
  25. flags: ACC_STATIC
  26. Code:
  27. stack=2, locals=1, args_size=0
  28. 0: bipush 100
  29. 2: putstatic #5 // Field a:I
  30. 5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
  31. 8: ldc #6 // String before init
  32. 10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  33. 13: iconst_3
  34. 14: iconst_0
  35. 15: idiv
  36. 16: istore_0
  37. 17: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
  38. 20: ldc #7 // String after init
  39. 22: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  40. 25: return
  41. LineNumberTable:
  42. line 2: 0
  43. line 4: 5
  44. line 5: 13
  45. line 6: 17
  46. line 7: 25
  47. }

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

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

  1. public static void main(String args[]){
  2. try{
  3. BadClass.doSomething();
  4. }catch (Throwable e){
  5. e.printStackTrace();
  6. }
  7.  
  8. BadClass.doSomething();
  9. }

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

  1. before init
  2. java.lang.ExceptionInInitializerError
  3. at ObjectTest.main(ObjectTest.java:7)
  4. at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  5. at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
  6. at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
  7. at java.lang.reflect.Method.invoke(Method.java:606)
  8. at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)
  9. Caused by: java.lang.ArithmeticException: / by zero
  10. at BadClass.<clinit>(ObjectTest.java:25)
  11. ... 6 more
  12. Exception in thread "main" java.lang.NoClassDefFoundError: Could not initialize class BadClass
  13. at ObjectTest.main(ObjectTest.java:12)
  14. at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  15. at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
  16. at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
  17. at java.lang.reflect.Method.invoke(Method.java:606)
  18. 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里对类的状态定义:

  1. enum ClassState {
  2. unparsable_by_gc = 0, // object is not yet parsable by gc. Value of _init_state at object allocation.
  3. allocated, // allocated (but not yet linked)
  4. loaded, // loaded and inserted in class hierarchy (but not linked yet)
  5. linked, // successfully linked/verified (but not initialized yet)
  6. being_initialized, // currently running class initializer
  7. fully_initialized, // initialized (successfull final state)
  8. initialization_error // error happened during initialization
  9. };

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

推荐阅读

这么流行的ZooKeeper,原来是这样设计的!

spring boot 引起的 “堆外内存泄漏”

 

版权声明:本文为perfma原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/perfma/p/12665251.html