前言
如果您看过这篇文章的话, 57 相互引用的类加载的调试, 那么还记得文章最末尾留下来的问题么 ?
细心的你, 可能会发现 code 运行时的字节码 和 class 文件中的字节码有一些不一样的地方 ?
发现没得, getstatic, putstatic, invokevirtual 的操作数运行时 和 class文件中 是不一样的, 这是咋回事呢, 这个是 这个index是在Rewriter阶段重写过的(这里的细节与本文没有关系, 暂时不展开)
那么 我们今天就来看一下这个问题,究竟是哪里重写了这些索引
以下代码, 截图基于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$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
运行时 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
rewriter.scan_method ?
在 rewriter.scan_method 里面打一个断点
首先是 rewrite 的步骤是在 类加载的 link 阶段
我们发现 当前扫描的方法 code_base 为 0x10a4cc480, 当前 code 为 0x10a4cc481
0x10a4cc480 处为 iconst_1, 断点处扫描到了 "putstatic #3"
进入 rewriter. rewrite_member_reference 方法, 我们发现 原来的操作数 3, 换成了 cache_index : 2
内存中的变化情况如下
(lldb) x 0x10a4cc480
0x10a4cc480: 04 b3 00 03 b2 00 04 b3 00 03 b1 ff 00 3e ff 08 .............>..
0x10a4cc490: 10 31 00 00 00 00 00 00 40 6f 63 09 01 00 00 00 .1......@oc.....
(lldb) x 0x10a4cc480
0x10a4cc480: 04 b3 02 00 b2 00 04 b3 00 03 b1 ff 00 3e ff 08 .............>..
0x10a4cc490: 10 31 00 00 00 00 00 00 40 6f 63 09 01 00 00 00 .1......@oc.....
可以看到 0x10a4cc482 开始两个字节, 00 03 换成 00 02
可以看到两点变化, 第一个是 操作数本身逻辑值发生了变化, 另外一点是 存储方式由 大端序 变成了 小端序
当这个方法的所有的字节码扫描完了之后, 就完成了 class 文件中的 code 到 内存中的 code 的转变了
呵呵 这个常量池索引 会影响到那些 字节码 呢?
case Bytecodes::_putstatic :
case Bytecodes::_putfield : {
// 省略了一部分和我们这里无关的代码
// fall through
case Bytecodes::_getstatic : // fall through
case Bytecodes::_getfield : // fall through
case Bytecodes::_invokevirtual : // fall through
case Bytecodes::_invokestatic :
case Bytecodes::_invokeinterface:
case Bytecodes::_invokehandle : // if reverse=true
rewrite_member_reference(bcp, prefix_length+1, reverse);
break;
rewriter.compute_index_maps
上面还有一个问题, 转换前后的索引有什么规则呢 ? 那我们接着往下看
int cp_entry_to_cp_cache(int i) { assert(has_cp_cache(i), "oob"); return _cp_map.at(i); }
老索引 -> 新索引是来自于 这个 _cp_map
_cp_map, _cp_cache_map 的计算来自于 rewriter.compute_index_maps
前者为 老索引 -> 新索引, 后者为 新索引 -> 老索引
这里将 JVM_CONSTANT_InterfaceMethodref, JVM_CONSTANT_Fieldref, JVM_CONSTANT_Methodref 重新排列, 构建了索引
[add at 2021.11.21]将 JVM_CONSTANT_String, JVM_CONSTANT_MethodHandle, JVM_CONSTANT_MethodType 重新排列, 构建了索引
Test06LoanRefEachOther$Clazz1 的常量池如下
/Users/jerry/ClionProjects/HelloOpenJdk/jdk9/build/macosx-x86_64-normal-serverANDclient-slowdebug/jdk/bin/java -da -dsa -Xint -Xmx10M -XX:+UseSerialGC com.hx.test04.Test06LoanRefEachOther
Signal: SIGSEGV (signal SIGSEGV)
Signal: SIGSEGV (signal SIGSEGV)
{constant pool}
- holder: 0x00000007c008fe28
- cache: 0x0000000000000000
- resolved_references: 0x0000000000000000
- reference_map: 0x0000000000000000
- 1 : Method : klass_index=5 name_and_type_index=30
- 2 : Method : klass_index=6 name_and_type_index=30
- 3 : Field : klass_index=5 name_and_type_index=31
- 4 : Field : klass_index=32 name_and_type_index=33
- 5 : Unresolved Class : 'com/hx/test04/Test06LoanRefEachOther$Clazz1'
- 6 : Unresolved Class : 'java/lang/Object'
- 7 : Utf8 : 'x1'
- 8 : Utf8 : 'I'
- 9 : Utf8 : 'f01'
- 10 : Utf8 : 'f02'
- 11 : Utf8 : 'f03'
- 12 : Utf8 : 'f04'
- 13 : Utf8 : 'f05'
- 14 : Utf8 : '<init>'
- 15 : Utf8 : '()V'
- 16 : Utf8 : 'Code'
- 17 : Utf8 : 'LineNumberTable'
- 18 : Utf8 : 'LocalVariableTable'
- 19 : Utf8 : 'this'
- 20 : Utf8 : 'Clazz1'
- 21 : Utf8 : 'InnerClasses'
- 22 : Utf8 : 'Lcom/hx/test04/Test06LoanRefEachOther$Clazz1;'
- 23 : Unresolved Class : 'com/hx/test04/Test06LoanRefEachOther$1'
- 24 : Utf8 : '(Lcom/hx/test04/Test06LoanRefEachOther$1;)V'
- 25 : Utf8 : 'x0'
- 26 : Utf8 : 'Lcom/hx/test04/Test06LoanRefEachOther$1;'
- 27 : Utf8 : '<clinit>'
- 28 : Utf8 : 'SourceFile'
- 29 : Utf8 : 'Test06LoanRefEachOther.java'
- 30 : NameAndType : name_index=14 signature_index=15
- 31 : NameAndType : name_index=7 signature_index=8
- 32 : Unresolved Class : 'com/hx/test04/Test06LoanRefEachOther$Clazz2'
- 33 : NameAndType : name_index=40 signature_index=8
- 34 : Unresolved Class : 'com/hx/test04/Test06LoanRefEachOther'
- 35 : Utf8 : 'com/hx/test04/Test06LoanRefEachOther$Clazz1'
- 36 : Utf8 : 'java/lang/Object'
- 37 : Utf8 : 'com/hx/test04/Test06LoanRefEachOther$1'
- 38 : Utf8 : 'com/hx/test04/Test06LoanRefEachOther$Clazz2'
- 39 : Utf8 : 'Clazz2'
- 40 : Utf8 : 'x2'
- 41 : Utf8 : 'com/hx/test04/Test06LoanRefEachOther'
可以发现 只有两个 Field, 两个 Method, 所以 图中 新索引 -> 老索引 的数量为 4
因为新索引从 0 开始, 索引 新旧索引偏移 -1
片尾彩蛋
如果配置了 RegisterFinalizersAtInit, 那么在 Object() 的构造方法后面 把 Bytecodes::_return 更新成了 Bytecodes::_return_register_finalizer
那么配置了 RegisterFinalizersAtInit 为 false 的情况, finalizer 在哪里注册呢 ?
在为对象分配空间的时候
HSDB 拿到的是 rewire 之前的字节码, 因此 RegisterFinalizersAtInit 为 true 只能在运行时验证了
(lldb) x 0x10dcc25a8
0x10dcc25a8: b1 ff 00 64 00 00 00 00 00 00 00 00 01 00 18 00 ...d............
0x10dcc25b8: 19 00 00 00 00 00 01 00 20 26 cc 0d 01 00 00 00 ........ &......
(lldb) x 0x10dcc25a8
0x10dcc25a8: e8 ff 00 64 00 00 00 00 00 00 00 00 01 00 18 00 ...d............
0x10dcc25b8: 19 00 00 00 00 00 01 00 20 26 cc 0d 01 00 00 00 ........ &......
======================= add at 2020.05.03 =======================
找到了一个之前的todo, 链接一下
R大曾经在这篇文章中 Java 8下如何查看JVM里Java应用的字节码? 介绍了 常量池索引 Rewrite 的相关只是
有些字节码指令的操作数在Class文件里跟在运行时看起来不同,是因为HotSpot VM在加载类的时候会对字节码做改写,把某些指令的操作数从常量池下标(constant pool index)改写为常量池缓存下标(constant pool cache index)。这是因为这些指令所需要引用的信息比一个constant pool entry slot要大,需要另外开一个更大的数据结构来放该常量池项的内容。
参考
57 相互引用的类加载的调试
Java 8下如何查看JVM里Java应用的字节码?