性能文章>05 相互引用的类加载的调试>

05 相互引用的类加载的调试原创

1年前
293855

前言 

呵呵, 最近看到了这样的一篇文档, Java类初始化的相互引用和次序问题 , 也是一个很经常看到的基础问题

但是 在以前的话, 我一定是思考那些理论来分析这个问题, 比如 main 中 读取 Clazz1.x1 导致了 Clazz1 的初始化, Clazz1 初始化的时候, 会使用到 Clazz2, 然后 Clazz2 初始化的时候回再次 触发 Clazz1 的初始化 

然后 这里出现了 相互依赖, 根据规范 5.5. Initialization, "If the Class object for C indicates that initialization is in progress for C by the current thread, then this must be a recursive request for initialization. Release LC and complete normally."

因此触发 Clazz1 的初始化正常完成了, 然后继续往下走 

那么 我们便来调试一下这个整个过程, 因为理论上的知道, 和实际调试起来 完全是两回事情, 也许会让你有一些 额外的收货 

一下代码, 截图 基于 jdk9 

首先是测试用例

package com.hx.test04;

/**
 * LoadRefEachOther
 *
 * @author Jerry.X.He <970655147@qq.com>
 * @version 1.0
 * @date 2020-03-08 10:58
 */
public class Test06LoanRefEachOther {

  // Test06LoanRefEachOther
  // refer : https://hllvm-group.iteye.com/group/topic/38847
  public static void main(String[] args) {

    System.out.println(Clazz1.x1);
//    System.out.println(Clazz2.x1);
    System.out.println(Clazz2.x2);

  }

  /**
   * Clazz1
   *
   * @author Jerry.X.He <970655147@qq.com>
   * @version 1.0
   * @date 2020-03-08 10:59
   */
  private static class Clazz1 {

    static int x1 = 1;
    int f01;
    int f02;
    int f03;
    int f04;
    int f05;

    static {
      x1 = Clazz2.x2;
    }

  }

  /**
   * Clazz2
   *
   * @author Jerry.X.He <970655147@qq.com>
   * @version 1.0
   * @date 2020-03-08 11:00
   */
  private static class Clazz2 extends Clazz1 {

    static int x2 = 2;
    int f01;
    int f02;
    int f03;
    int f04;
    int f05;

    static {
      x2 = Clazz1.x1;
    }

  }

}

以及运行结果 

按照 上面的理论上的思考, 输出的结果, 也是这样 

再上一部分下文需要参考的字节码 

Test06LoanRefEachOther.main 

Test06LoanRefEachOther
  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: getstatic     #3                  // Field com/hx/test04/Test06LoanRefEachOther$Clazz1.x1:I
       6: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
       9: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      12: getstatic     #5                  // Field com/hx/test04/Test06LoanRefEachOther$Clazz2.x2:I
      15: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
      18: return

 Test06LoanRefEachOther$Clazz1.<clinit>

Clazz1
  static {};
    Code:
       0: iconst_1
       1: putstatic     #3                  // Field x1:I
       4: getstatic     #4                  // Field com/hx/test04/Test06LoanRefEachOther$Clazz2.x2:I
       7: putstatic     #3                  // Field x1:I
      10: return

 Test06LoanRefEachOther$Clazz2.<clinit>

Clazz2
  static {};
    Code:
       0: iconst_2
       1: putstatic     #2                  // Field x2:I
       4: getstatic     #3                  // Field com/hx/test04/Test06LoanRefEachOther$Clazz1.x1:I
       7: putstatic     #2                  // Field x2:I
      10: return

准备的东西都这些了, 那么接下来 便是主题了 

第一次初始化

可以看到, 第一次初始化的是 Clazz1, 并且是在 main 方法中初始化的 

那么具体触发 Clazz1 初始化是哪一个字节码呢 ? 

这个就需要我们查看下 当前运行时的情况了 

我们打印一下 main方法 code 的相关信息 

(lldb) print method->code_base()
(address) $240 = 0x00000001042cbf48 "\xffffffb2\x01"
(lldb) print method->code_size()
(int) $241 = 19
(lldb) x 0x00000001042cbf48 
0x1042cbf48: b2 01 00 b2 02 00 b6 03 00 b2 01 00 b2 04 00 b6  ................
0x1042cbf58: 03 00 b1 ff 00 1e 4a 4a 00 00 00 00 13 00 17 00  ......JJ........

# code 拆解如下
0x1042cbf48: b2 01 00: getstatic #1 // Field java/lang/System.out:Ljava/io/PrintStream;
0x1042cbf4b: b2 02 00: getstatic #2 // Field com/hx/test04/Test06LoanRefEachOther$Clazz1.x1:I
0x1042cbf4e: b6 03 00: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
0x1042cbf51: b2 01 00: getstatic #1 // Field java/lang/System.out:Ljava/io/PrintStream;
0x1042cbf54: b2 04 00: getstatic #4 // Field com/hx/test04/Test06LoanRefEachOther$Clazz2.x2:I
0x1042cbf57: b6 03 00: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
0x1042cbf5a: b1: return

 

再查看一下 r13 寄存器, 指向的是 0x00000001042cbf4b 

(lldb) re r
General Purpose Registers:
       rbx = 0x00000000000000b2
       rbp = 0x00007000100d0698
       rsp = 0x00007000100d0640
       r12 = 0x0000000000000000
       r13 = 0x00000001042cbf4b
       r14 = 0x00007000100d06a8
       r15 = 0x00007fb91180f800
       rip = 0x0000000108071120
13 registers were unavailable.

对应的是 "getstatic #2 // Field com/hx/test04/Test06LoanRefEachOther$Clazz1.x1:I" 

因此 第一次加载是在 main 里面的 "System.out.println(Clazz1.x1);" 触发加载的 

第二次初始化

第二次的初始化, 同样初始化的是 Clazz1, 是在 Clazz1 的类初始化方法里面 

 

我们打印一下 Clazz1.<clinit>方法 code 的相关信息 

(lldb) print method->code_base()
(address) $247 = 0x00000001042cc480 "\x04\xffffffb3\x02"
(lldb) print method->code_size()
(int) $248 = 11
(lldb) x 0x00000001042cc480
0x1042cc480: 04 b3 02 00 b2 03 00 b3 02 00 b1 ff 00 3c ff 08  .............<..
0x1042cc490: 10 31 00 00 00 00 00 00 40 6f 43 03 01 00 00 00  .1......@oC.....


# code 拆解如下
0x1042cc480: 04: iconst_1
0x1042cc481: b3 02 00: putstatic #2 // Field x1:I
0x1042cc484: b2 03 00: getstatic #3 // Field com/hx/test04/Test06LoanRefEachOther$Clazz2.x2:I
0x1042cc487: b3 02 00: putstatic #2 // Field x1:I
0x1042cc48a: b1: return

我们再看一下 r13 寄存器 

(lldb) re r
General Purpose Registers:
       rbx = 0x00000000000000b3
       rbp = 0x00007000100cf600
       rsp = 0x00007000100cf5a0
       r12 = 0x0000000000000000
       r13 = 0x00000001042cc481
       r14 = 0x00007000100cf608
       r15 = 0x00007fb91180f800
       rip = 0x0000000108071632
13 registers were unavailable.

对应的是 "putstatic #2 // Field x1:I", 并且这里的初始化 是由于 main 方法中 Clazz1 的初始化, 进而调用 Clazz1.<clinit>, 进而再一次 尝试初始化 Clazz1, 这一次的调用出口是  

第三次初始化

这次初始化的是 Clazz2, 然后 触发的地方是 Clazz1.<clinit> 里面 

呵呵 相信这一次 具体的调用的地方在哪里, 大家也能猜出来了吧  

(lldb) re r
General Purpose Registers:
       rbx = 0x00000000000000b2
       rbp = 0x00007000100cf600
       rsp = 0x00007000100cf5b0
       r12 = 0x0000000000000000
       r13 = 0x00000001042cc484
       r14 = 0x00007000100cf608
       r15 = 0x00007fb91180f800
       rip = 0x0000000108071120
13 registers were unavailable.

第四次初始化

这次是初始化 Clazz1, 触发的地方是在 Clazz2 的初始化需要初始化 其父类 Clazz1 

这一次的调用出口是  递归初始化 complete normally. 

第五次初始化

这一次初始化的是 Clazz2, 触发的地方是在 Clazz2.<clinit> 方法里面 

 

我们打印一下 Clazz2.<clinit>方法 code 的相关信息 

(lldb) print method->code_base()
(address) $279 = 0x00000001042cc898 "\x05\xffffffb3\x01"
(lldb) print method->code_size()
(int) $280 = 11
(lldb) x 0x00000001042cc898
0x1042cc898: 05 b3 01 00 b2 02 00 b3 01 00 b1 ff 00 68 ff 08  .............h..
0x1042cc8a8: 10 31 00 00 00 00 00 00 40 6f 43 03 01 00 00 00  .1......@oC.....

# code 拆解如下
0x1042cc898: 05: iconst_2
0x1042cc899: b3 01 00: putstatic #1 // Field x2:I
0x1042cc89c: b2 02 00: getstatic #2 // Field com/hx/test04/Test06LoanRefEachOther$Clazz1.x1:I
0x1042cc89f: b3 01 00: putstatic #1 // Field x2:I
0x1042cc8a2: b1: return 

我们再看一下 r13 寄存器 

(lldb) re  r
General Purpose Registers:
       rbx = 0x00000000000000b3
       rbp = 0x00007000100ce570
       rsp = 0x00007000100ce510
       r12 = 0x0000000000000000
       r13 = 0x00000001042cc899
       r14 = 0x00007000100ce578
       r15 = 0x00007fb91180f800
       rip = 0x0000000108071632
13 registers were unavailable.

这次加载处理的时候 对应的字节码是 "putstatic #2 // Field x2:I" 

这一次的调用出口是  递归初始化 complete normally. 

第六次初始化

这一次加载的是 Clazz1, 触发的地方是在 Clazz2.<clinit> 里面 

查看一下 r13 寄存器, 具体的字节码是在 

(lldb) re r
General Purpose Registers:
       rbx = 0x00000000000000b2
       rbp = 0x00007000100ce570
       rsp = 0x00007000100ce520
       r12 = 0x0000000000000000
       r13 = 0x00000001042cc89c
       r14 = 0x00007000100ce578
       r15 = 0x00007fb91180f800
       rip = 0x0000000108071120
13 registers were unavailable.

这一次的调用出口是  递归初始化 complete normally. 

 

 

第七次初始化

这一次加载的是 Clazz2, 触发的地方是在 Clazz2.<clinit> 里面 

查看一下 r13 寄存器, 具体的字节码是在 

(lldb) re r
General Purpose Registers:
       rbx = 0x00000000000000b3
       rbp = 0x00007000100ce570
       rsp = 0x00007000100ce510
       r12 = 0x0000000000000000
       r13 = 0x00000001042cc89f
       r14 = 0x00007000100ce578
       r15 = 0x00007fb91180f800
       rip = 0x0000000108071632
13 registers were unavailable.

这一次的调用出口是  递归初始化 complete normally. 

这里执行的便是 Clazz2.<clinit> 里面倒数第二个语句, putstatic 

当吧 Clazz2.<clinit> 执行完成, 再把后面的 initialize_impl 的相关业务处理完成, Clazz2 的初始化也就完成了 

第八次初始化

这一次加载的是 Clazz1, 触发的地方是在 Clazz1.<clinit> 里面 

到这里就说明 Clazz2 初始化完成了, 目前是到了 "x1 = Clazz2.x2;" 的赋值处理这里了 

查看一下 r13 寄存器, 具体的字节码是在 

(lldb) re r
General Purpose Registers:
       rbx = 0x00000000000000b3
       rbp = 0x00007000100cf600
       rsp = 0x00007000100cf5a0
       r12 = 0x0000000000000000
       r13 = 0x00000001042cc487
       r14 = 0x00007000100cf608
       r15 = 0x00007fb91180f800
       rip = 0x0000000108071632
13 registers were unavailable.

这里执行的便是 Clazz1.<clinit> 里面倒数第二个语句, putstatic 

当把Clazz1.<clinit> 执行完成, 再把后面的 initialize_impl 的相关业务处理完成, Clazz1 的初始化也就完成了 

呵呵, 不知道这和各位开始看到这个问题的时候 思考的是否一样呢 ?

细心的你, 可能会发现 code 运行时的字节码和 class 文件中的字节码有一些不一样的地方 ?

发现没得, getstatic, putstatic, invokevirtual 的操作数运行时 和 class文件中 是不一样的, 这是咋回事呢, 这个是这个index是在Rewriter阶段重写过的(这里的细节与本文没有关系, 暂时不展开) 

参考

Java类初始化的相互引用和次序问题 

点赞收藏
分类:标签:
黄金键盘
请先登录,查看5条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

随机一门技术分享之Netty

随机一门技术分享之Netty

从 Linux 内核角度探秘 JDK MappedByteBuffer

从 Linux 内核角度探秘 JDK MappedByteBuffer

MappedByteBuffer VS FileChannel:从内核层面对比两者的性能差异

MappedByteBuffer VS FileChannel:从内核层面对比两者的性能差异

5
5