【JVM源码解析】模板解释器解释执行Java字节码指令(下)

java

本文由HeapDump性能社区首席讲师鸠摩(马智)授权整理发布

第22篇-虚拟机字节码之运算指令

虚拟机规范中与运算相关的字节码指令如下表所示。

0x60iadd将栈顶两int型数值相加并将结果压入栈顶
0x61ladd将栈顶两long型数值相加并将结果压入栈顶
0x62fadd将栈顶两float型数值相加并将结果压入栈顶
0x63dadd将栈顶两double型数值相加并将结果压入栈顶
0x64isub将栈顶两int型数值相减并将结果压入栈顶
0x65lsub将栈顶两long型数值相减并将结果压入栈顶
0x66fsub将栈顶两float型数值相减并将结果压入栈顶
0x67dsub将栈顶两double型数值相减并将结果压入栈顶
0x68imul将栈顶两int型数值相乘并将结果压入栈顶
0x69lmul将栈顶两long型数值相乘并将结果压入栈顶
0x6afmul将栈顶两float型数值相乘并将结果压入栈顶
0x6bdmul将栈顶两double型数值相乘并将结果压入栈顶
0x6cidiv将栈顶两int型数值相除并将结果压入栈顶
0x6dldiv将栈顶两long型数值相除并将结果压入栈顶
0x6efdiv将栈顶两float型数值相除并将结果压入栈顶
0x6fddiv将栈顶两double型数值相除并将结果压入栈顶
0x70irem将栈顶两int型数值作取模运算并将结果压入栈顶
0x71lrem将栈顶两long型数值作取模运算并将结果压入栈顶
0x72frem将栈顶两float型数值作取模运算并将结果压入栈顶
0x73drem将栈顶两double型数值作取模运算并将结果压入栈顶
0x74ineg将栈顶int型数值取负并将结果压入栈顶
0x75lneg将栈顶long型数值取负并将结果压入栈顶
0x76fneg将栈顶float型数值取负并将结果压入栈顶
0x77dneg将栈顶double型数值取负并将结果压入栈顶
0x78ishl将int型数值左移位指定位数并将结果压入栈顶
0x79lshl将long型数值左移位指定位数并将结果压入栈顶
0x7aishr将int型数值右(符号)移位指定位数并将结果压入栈顶
0x7blshr将long型数值右(符号)移位指定位数并将结果压入栈顶
0x7ciushr将int型数值右(无符号)移位指定位数并将结果压入栈顶
0x7dlushr将long型数值右(无符号)移位指定位数并将结果压入栈顶
0x7eiand将栈顶两int型数值作“按位与”并将结果压入栈顶
0x7fland将栈顶两long型数值作“按位与”并将结果压入栈顶
0x80ior将栈顶两int型数值作“按位或”并将结果压入栈顶
0x81lor将栈顶两long型数值作“按位或”并将结果压入栈顶
0x82ixor将栈顶两int型数值作“按位异或”并将结果压入栈顶
0x83lxor将栈顶两long型数值作“按位异或”并将结果压入栈顶
0x84iinc将指定int型变量增加指定值(i++、i--、i+=2)
0x94lcmp比较栈顶两long型数值大小,并将结果(1、0或-1)压入栈顶
0x95fcmpl比较栈顶两float型数值大小,并将结果(1、0或-1)压入栈顶;当其中一个数值为NaN时,将-1压入栈顶
0x96fcmpg比较栈顶两float型数值大小,并将结果(1、0或-1)压入栈顶;当其中一个数值为NaN时,将1压入栈顶
0x97dcmpl比较栈顶两double型数值大小,并将结果(1、0或-1)压入栈顶;当其中一个数值为NaN时,将-1压入栈顶
0x98dcmpg比较栈顶两double型数值大小,并将结果(1、0或-1)压入栈顶;当其中一个数值为NaN时,将1压入栈顶

1、基本加、减、乘与除指令

1、iadd指令

iadd指令将两个栈顶的整数相加,然后将相加的结果压入栈顶,其指令的格式如下:

iadd  val1,val2 

val1与val2表示两个int类型的整数,在指令执行时,将val1与val3从操作数栈中出栈,将这两个数值相加得到 int 类型数据 result,将result压入操作数栈中。

iadd指令的模板定义如下:

def(Bytecodes::_iadd , ____|____|____|____, itos, itos, iop2 , add);

生成函数为TemplateTable::iop2(),实现如下:

void TemplateTable::iop2(Operation op) {

switch (op) {

case add : __ pop_i(rdx); __ addl (rax, rdx); break;

case sub : __ movl(rdx, rax); __ pop_i(rax); __ subl (rax, rdx); break;

case mul : __ pop_i(rdx); __ imull(rax, rdx); break;

case _and : __ pop_i(rdx); __ andl (rax, rdx); break;

case _or : __ pop_i(rdx); __ orl (rax, rdx); break;

case _xor : __ pop_i(rdx); __ xorl (rax, rdx); break;

case shl : __ movl(rcx, rax); __ pop_i(rax); __ shll (rax); break;

case shr : __ movl(rcx, rax); __ pop_i(rax); __ sarl (rax); break;

case ushr : __ movl(rcx, rax); __ pop_i(rax); __ shrl (rax); break;

default : ShouldNotReachHere();

}

}

可以看到,这个函数是许多指令的生成函数,如iadd、isub、imul、iand、ior、ixor、ishl、ishr、iushr。

为iadd指令生成的汇编代码如下:

mov    (%rsp),%edx

add $0x8,%rsp

add %edx,%eax

将栈顶与栈顶中缓存的%eax相加后的结果存储到%eax中。 

2、isub指令 

isub指令生成的汇编代码如下:

mov    %eax,%edx

mov (%rsp),%eax

add $0x8,%rsp

sub %edx,%eax

代码实现比较简单,这里不再介绍。 

3、idiv指令

idiv是字节码除法指令,这个指令的格式如下:

idiv val1,val2

val1 和 val2 都必须为 int 类型数据,指令执行时,val1 和 val2从操作数栈中出栈,并且将这两个数值相除(val1÷val2),结果转换为 int 类型值 result,最后 result 被压入到操作数栈中。

idiv指令的模板定义如下:

def(Bytecodes::_idiv , ____|____|____|____, itos, itos, idiv ,  _  );

调用的生成函数为TemplateTable::idiv(),生成的汇编如下:

0x00007fffe1019707: mov    %eax,%ecx

0x00007fffe1019709: mov (%rsp),%eax

0x00007fffe101970c: add $0x8,%rsp

// 测试一下被除数是否为0x80000000,如果不是,就跳转到normal_case

0x00007fffe1019710: cmp $0x80000000,%eax

0x00007fffe1019716: jne 0x00007fffe1019727

// 被除数是0x80000000,而除数如果是-1的话,则跳转到special_case

0x00007fffe101971c: xor %edx,%edx

0x00007fffe101971e: cmp $0xffffffff,%ecx

0x00007fffe1019721: je 0x00007fffe101972a

// -- normal_case --

// cltd将eax寄存器中的数据符号扩展到edx:eax,具体就是

// 把eax的32位整数扩展为64位,高32位用eax的符号位填充保存到edx

0x00007fffe1019727: cltd

0x00007fffe1019728: idiv %ecx

// -- special_case --

其中idiv函数会使用规定的寄存器,如下图所示。

汇编对0x80000000 / -1 这个特殊的除法做了检查。参考:利用反汇编调试与补码解释0x80000000 / -1整形输出异常不一致

2、比较指令

lcmp指令比较栈顶两long型数值大小,并将结果(1、0或-1)压入栈顶。指令的格式如下:

lcmp val1,val2

val1 和 val2 都必须为 long 类型数据,指令执行时, val1 和 val2从操作数栈中出栈,使用一个 int 数值作为比较结果:

  • 如果 val1 大于val2,结果为 1;
  • 如果 val1 等于 val2,结果为 0;
  • 如果 val1小于 val2,结果为-1。

最后比较结果被压入到操作数栈中。

lcmp字节码指令的模板定义如下:

def(Bytecodes::_lcmp , ____|____|____|____, ltos, itos, lcmp ,  _ );

生成函数为TemplateTable::lcmp(), 生成的汇编如下:

0x00007fffe101a6c8: mov     (%rsp),%rdx

0x00007fffe101a6cc: add $0x10,%rsp

// cmp指令描述如下:

// 第1操作数<第2操作数时,ZF=0

// 第1操作数=第2操作数时,ZF=1

// 第1操作数>第2操作数时,ZF=0

0x00007fffe101a6d0: cmp %rax,%rdx

0x00007fffe101a6d3: mov $0xffffffff,%eax // 将-1移到%eax中

// 如果第1操作数小于第2操作数就跳转到done

0x00007fffe101a6d8: jl 0x00007fffe101a6e0

// cmp指令执行后,执行setne指令就能获取比较的结果

// 根据eflags中的状态标志(CF,SF,OF,ZF和PF)将目标操作数设置为0或1

0x00007fffe101a6da: setne %al

0x00007fffe101a6dd: movzbl %al,%eax

// -- done --

如上汇编代码的逻辑非常简单,这里不再介绍。

关于其它字节码指令的逻辑也相对简单,有兴趣的可自行研究。

第23篇-虚拟机字节码指令之类型转换

Java虚拟机规范中定义的类型转换相关的字节码指令如下表所示。

0x85i2l将栈顶int型数值强制转换成long型数值并将结果压入栈顶
0x86i2f将栈顶int型数值强制转换成float型数值并将结果压入栈顶
0x87i2d将栈顶int型数值强制转换成double型数值并将结果压入栈顶
0x88l2i将栈顶long型数值强制转换成int型数值并将结果压入栈顶
0x89l2f将栈顶long型数值强制转换成float型数值并将结果压入栈顶
0x8al2d将栈顶long型数值强制转换成double型数值并将结果压入栈顶
0x8bf2i将栈顶float型数值强制转换成int型数值并将结果压入栈顶
0x8cf2l将栈顶float型数值强制转换成long型数值并将结果压入栈顶
0x8df2d将栈顶float型数值强制转换成double型数值并将结果压入栈顶
0x8ed2i将栈顶double型数值强制转换成int型数值并将结果压入栈顶
0x8fd2l将栈顶double型数值强制转换成long型数值并将结果压入栈顶
0x90d2f将栈顶double型数值强制转换成float型数值并将结果压入栈顶
0x91i2b将栈顶int型数值强制转换成byte型数值并将结果压入栈顶
0x92i2c将栈顶int型数值强制转换成char型数值并将结果压入栈顶
0x93i2s将栈顶int型数值强制转换成short型数值并将结果压入栈顶

上表字节码指令的模板定义如下:

def(Bytecodes::_i2l   , ____|____|____|____, itos, ltos, convert ,  _           );

def(Bytecodes::_i2f , ____|____|____|____, itos, ftos, convert , _ );

def(Bytecodes::_i2d , ____|____|____|____, itos, dtos, convert , _ );

def(Bytecodes::_l2i , ____|____|____|____, ltos, itos, convert , _ );

def(Bytecodes::_l2f , ____|____|____|____, ltos, ftos, convert , _ );

def(Bytecodes::_l2d , ____|____|____|____, ltos, dtos, convert , _ );

def(Bytecodes::_f2i , ____|____|____|____, ftos, itos, convert , _ );

def(Bytecodes::_f2l , ____|____|____|____, ftos, ltos, convert , _ );

def(Bytecodes::_f2d , ____|____|____|____, ftos, dtos, convert , _ );

def(Bytecodes::_d2i , ____|____|____|____, dtos, itos, convert , _ );

def(Bytecodes::_d2l , ____|____|____|____, dtos, ltos, convert , _ );

def(Bytecodes::_d2f , ____|____|____|____, dtos, ftos, convert , _ );

def(Bytecodes::_i2b , ____|____|____|____, itos, itos, convert , _ );

def(Bytecodes::_i2c , ____|____|____|____, itos, itos, convert , _ );

def(Bytecodes::_i2s , ____|____|____|____, itos, itos, convert , _ ); 

相关字节码转换指令的生成函数为TemplateTable::convert(),此函数的实现如下:

void TemplateTable::convert() {

static const int64_t is_nan = 0x8000000000000000L;

// Conversion

switch (bytecode()) {

case Bytecodes::_i2l:

__ movslq(rax, rax);

break;

case Bytecodes::_i2f:

__ cvtsi2ssl(xmm0, rax);

break;

case Bytecodes::_i2d:

__ cvtsi2sdl(xmm0, rax);

break;

case Bytecodes::_i2b:

__ movsbl(rax, rax);

break;

case Bytecodes::_i2c:

__ movzwl(rax, rax);

break;

case Bytecodes::_i2s:

__ movswl(rax, rax);

break;

case Bytecodes::_l2i:

__ movl(rax, rax);

break;

case Bytecodes::_l2f:

__ cvtsi2ssq(xmm0, rax);

break;

case Bytecodes::_l2d:

__ cvtsi2sdq(xmm0, rax);

break;

case Bytecodes::_f2i:

{

Label L;

__ cvttss2sil(rax, xmm0);

__ cmpl(rax, 0x80000000); // NaN or overflow/underflow?

__ jcc(Assembler::notEqual, L);

__ call_VM_leaf(CAST_FROM_FN_PTR(address, SharedRuntime::f2i), 1);

__ bind(L);

}

break;

case Bytecodes::_f2l:

{

Label L;

__ cvttss2siq(rax, xmm0);

// NaN or overflow/underflow?

__ cmp64(rax, ExternalAddress((address) &is_nan));

__ jcc(Assembler::notEqual, L);

__ call_VM_leaf(CAST_FROM_FN_PTR(address, SharedRuntime::f2l), 1);

__ bind(L);

}

break;

case Bytecodes::_f2d:

__ cvtss2sd(xmm0, xmm0);

break;

case Bytecodes::_d2i:

{

Label L;

__ cvttsd2sil(rax, xmm0);

__ cmpl(rax, 0x80000000); // NaN or overflow/underflow?

__ jcc(Assembler::notEqual, L);

__ call_VM_leaf(CAST_FROM_FN_PTR(address, SharedRuntime::d2i), 1);

__ bind(L);

}

break;

case Bytecodes::_d2l:

{

Label L;

__ cvttsd2siq(rax, xmm0);

// NaN or overflow/underflow?

__ cmp64(rax, ExternalAddress((address) &is_nan));

__ jcc(Assembler::notEqual, L);

__ call_VM_leaf(CAST_FROM_FN_PTR(address, SharedRuntime::d2l), 1);

__ bind(L);

}

break;

case Bytecodes::_d2f:

__ cvtsd2ss(xmm0, xmm0);

break;

default:

ShouldNotReachHere();

}

}

如_i2l指令将栈顶int型数值强制转换成long型数值并将结果压入栈顶,其对应的汇编代码如下:

movslq %eax,%rax  // 将一个双字扩展后送到一个四字中

对于浮点数float或long转int或long类型相对复杂,下面看一个float转int类型的f2i指令。

// 把标量单精度数转换为占用双字的标量整数

0x00007fffe1019189: vcvttss2si %xmm0,%eax

// 和0x80000000进行比较,如果不相等,则跳转到L

0x00007fffe101918d: cmp $0x80000000,%eax

0x00007fffe1019193: jne 0x00007fffe10191bc

// 如果栈顶指针已经按16字节对齐,则可直接调用调用SharedRuntime::f2i()函数,否则

// 将栈顶指令按16字节对齐后再调用

0x00007fffe1019199: test $0xf,%esp

0x00007fffe101919f: je 0x00007fffe10191b7

0x00007fffe10191a5: sub $0x8,%rsp

// 调用SharedRuntime::f2i()函数

0x00007fffe10191a9: callq 0x00007ffff6a0f946

0x00007fffe10191ae: add $0x8,%rsp

0x00007fffe10191b2: jmpq 0x00007fffe10191bc

// 调用SharedRuntime::f2i()函数

0x00007fffe10191b7: callq 0x00007ffff6a0f946

---- L ----

生成的汇编指令vcvttss2si的意思为把标量单精度数转换为占用双字的标量整数,名称的由来解读如下:

cvt:convert,转换;

t:truncation,截断;

ss:scalar single,标量单精度数;

2:to;

si:scalar integer,标量整数。

调用的SharedRuntime::f2i()函数的实现如下:

JRT_LEAF(jint, SharedRuntime::f2i(jfloat  x))

if (g_isnan(x)) // 如果为非数字值,直接返回0

return 0;

if (x >= (jfloat) max_jint)

return max_jint;

if (x <= (jfloat) min_jint)

return min_jint;

return (jint) x;

JRT_END

C++函数调用时,需要一个参数x,在GNU / Linux上遵循System V AMD64 ABI的调用约定。寄存器RDI,RSI,RDX,RCX,R8和R9是用于整数和存储器地址的参数和XMM0,XMM1,XMM2,XMM3,XMM4,XMM5,XMM6和XMM7用于浮点参数,所以将用xmm0做为第1个参数,这个参数恰好是栈顶用来缓存浮点数的寄存器,所以默认不用任何操作。 

返回值存储到%rax中,由于tos_out为itos,%rax寄存器用来做栈顶缓存,所以也不需要做额外的操作。

第24篇-虚拟机对象操作指令之getstatic

Java虚拟机规范中定义的对象操作相关的字节码指令如下表所示。

0xb2getstatic获取指定类的静态域,并将其值压入栈顶
0xb3putstatic为指定的类的静态域赋值
0xb4getfield获取指定类的实例域,并将其值压入栈顶
0xb5putfield为指定的类的实例域赋值
0xbbnew创建一个对象,并将其引用值压入栈顶
0xbcnewarray创建一个指定原始类型(如int,、float,、char等)的数组,并将其引用值压入栈顶
0xbdanewarray创建一个引用型(如类、接口或数组)的数组,并将其引用值压入栈顶
0xbearraylength获得数组的长度值并压入栈顶
0xc0checkcast检验类型转换,检验未通过将抛出ClassCastException
0xc1instanceof检验对象是否是指定的类的实例,如果是将1压入栈顶,否则将0压入栈顶
0xc5multianewarray创建指定类型和指定维度的多维数组(执行该指令时,操作栈中必须包含各维度的长度值),并将其引用值压入栈顶

字节码指令的模板定义如下:

def(Bytecodes::_getstatic           , ubcp|____|clvm|____, vtos, vtos, getstatic           , f1_byte      );

def(Bytecodes::_putstatic , ubcp|____|clvm|____, vtos, vtos, putstatic , f2_byte );

def(Bytecodes::_getfield , ubcp|____|clvm|____, vtos, vtos, getfield , f1_byte );

def(Bytecodes::_putfield , ubcp|____|clvm|____, vtos, vtos, putfield , f2_byte );

def(Bytecodes::_new , ubcp|____|clvm|____, vtos, atos, _new , _ );

def(Bytecodes::_newarray , ubcp|____|clvm|____, itos, atos, newarray , _ );

def(Bytecodes::_anewarray , ubcp|____|clvm|____, itos, atos, anewarray , _ );

def(Bytecodes::_multianewarray , ubcp|____|clvm|____, vtos, atos, multianewarray , _ );

def(Bytecodes::_arraylength , ____|____|____|____, atos, itos, arraylength , _ );

def(Bytecodes::_checkcast , ubcp|____|clvm|____, atos, atos, checkcast , _ );

def(Bytecodes::_instanceof , ubcp|____|clvm|____, atos, itos, instanceof , _ );

new字节码指令的生成函数为TemplateTable::_new(),这在《深入剖析Java虚拟机:源码剖析与实例详解(基础卷)》的第9章类对象创建时详细介绍过,这里不再介绍。

getstatic字节码指令获取指定类的静态域,并将其值压入栈顶。格式如下:

getstatic indexbyte1 indexbyte2

无符号数indexbyte1和indexbyte2构建为(indexbyte1<<8)|indexbyte2,这个值指明了一个当前类的运行时常量池索引值,指向的运行时常量池项为一个字段的符号引用。

getstatic字节码指令的生成函数为TemplateTable::getstatic(),还有个类似的getfield指令,这些生成函数如下:

void TemplateTable::getfield(int byte_no) {

getfield_or_static(byte_no, false); // getfield的byte_no值为1

}

void TemplateTable::getstatic(int byte_no) {

getfield_or_static(byte_no, true); // getstatic的byte_no的值为1

}

最终都会调用getfield_or_static()函数生成机器指令片段。此函数生成的机器指令片段对应的汇编代码如下:

// 获取ConstantPoolCache中ConstantPoolCacheEntry的index

0x00007fffe101fd10: movzwl 0x1(%r13),%edx

// 从栈中获取ConstantPoolCache的首地址

0x00007fffe101fd15: mov -0x28(%rbp),%rcx

// 左移2位,因为%edx中存储的是ConstantPoolCacheEntry index,

// 左移2位是因为ConstantPoolCacheEntry的内存占用是4个字

0x00007fffe101fd19: shl $0x2,%edx

// 计算%rcx+%rdx*8+0x10,获取ConstantPoolCacheEntry[_indices,_f1,_f2,_flags]中的_indices

// 因为ConstantPoolCache的大小为0x16字节,%rcx+0x10定位到第一个ConstantPoolCacheEntry的开始位置

// %rdx*8算出来的是相对于第一个ConstantPoolCacheEntry的字节偏移

0x00007fffe101fd1c: mov 0x10(%rcx,%rdx,8),%ebx

// _indices向右移动16位后获取[get bytecode,set bytecode,original constant pool index]中的get bytecode与set bytecode

0x00007fffe101fd20: shr $0x10,%ebx

// 获取set bytecode字段的值

0x00007fffe101fd23: and $0xff,%ebx

// 0xb2是getstatic指令的Opcode,比较值,如果相等就说明已经连接,跳转到resolved

0x00007fffe101fd29: cmp $0xb2,%ebx

0x00007fffe101fd2f: je 0x00007fffe101fdce

// 将getstatic字节码的Opcode存储到%ebx中

0x00007fffe101fd35: mov $0xb2,%ebx

// 省略通过调用MacroAssembler::call_VM()函数来执行InterpreterRuntime::resolve_get_put()函数的汇编代码

// ...

调用MacroAssembler::call_VM()函数生成如下代码,通过这些代码来执行InterpreterRuntime::resolve_get_put()函数。MacroAssembler::call_VM()函数的汇编在之前已经详细介绍过,这里不再介绍,直接给出汇编代码,如下:

0x00007fffe101fd3a: callq  0x00007fffe101fd44

0x00007fffe101fd3f: jmpq 0x00007fffe101fdc2

0x00007fffe101fd44: mov %rbx,%rsi

0x00007fffe101fd47: lea 0x8(%rsp),%rax

0x00007fffe101fd4c: mov %r13,-0x38(%rbp)

0x00007fffe101fd50: mov %r15,%rdi

0x00007fffe101fd53: mov %rbp,0x200(%r15)

0x00007fffe101fd5a: mov %rax,0x1f0(%r15)

0x00007fffe101fd61: test $0xf,%esp

0x00007fffe101fd67: je 0x00007fffe101fd7f

0x00007fffe101fd6d: sub $0x8,%rsp

0x00007fffe101fd71: callq 0x00007ffff66b567c

0x00007fffe101fd76: add $0x8,%rsp

0x00007fffe101fd7a: jmpq 0x00007fffe101fd84

0x00007fffe101fd7f: callq 0x00007ffff66b567c

0x00007fffe101fd84: movabs $0x0,%r10

0x00007fffe101fd8e: mov %r10,0x1f0(%r15)

0x00007fffe101fd95: movabs $0x0,%r10

0x00007fffe101fd9f: mov %r10,0x200(%r15)

0x00007fffe101fda6: cmpq $0x0,0x8(%r15)

0x00007fffe101fdae: je 0x00007fffe101fdb9

0x00007fffe101fdb4: jmpq 0x00007fffe1000420

0x00007fffe101fdb9: mov -0x38(%rbp),%r13

0x00007fffe101fdbd: mov -0x30(%rbp),%r14

0x00007fffe101fdc1: retq

如上代码完成的事情很简单,就是调用C++函数编写的InterpreterRuntime::resolve_get_put()函数,此函数会填充常量池缓存中ConstantPoolCacheEntry信息,关于ConstantPoolCache以及ConstantPoolCacheEntry,还有ConstantPoolCacheEntry中各个字段的含义在《深入剖析Java虚拟机:源码剖析与实例详解(基础卷)》中已经详细介绍过,这里不再介绍。

InterpreterRuntime::resolve_get_put()函数的实现比较多,我们首先看一部分实现,如下:

IRT_ENTRY(void, InterpreterRuntime::resolve_get_put(JavaThread* thread, Bytecodes::Code bytecode))

// resolve field

fieldDescriptor info;

constantPoolHandle pool(thread, method(thread)->constants());

bool is_put = (bytecode == Bytecodes::_putfield || bytecode == Bytecodes::_putstatic);

bool is_static = (bytecode == Bytecodes::_getstatic || bytecode == Bytecodes::_putstatic);

{

JvmtiHideSingleStepping jhss(thread);

int x = get_index_u2_cpcache(thread, bytecode); // 根据线程栈中的bcp来获取常量池缓存索引

LinkResolver::resolve_field_access(info, pool, x ,bytecode, CHECK); // 向info中收集信息

}

// check if link resolution caused cpCache to be updated

if (already_resolved(thread)){

return;

}

...

}

调用get_index_u2_cpcache()函数从当前方法对应的栈帧中获取bcp,然后通过bcp来获取字节码指令的操作数,也就是常量池索引,得到常量池索引后调用LinkResolver::resolve_field_access()函数可能会连接类和字段,然后将查询到的字段相关信息存储到fieldDescriptor中。resolve_field_access()函数的实现如下:

void LinkResolver::resolve_field_access(

fieldDescriptor& result,

constantPoolHandle pool,

int index, // 常量池索引

Bytecodes::Code byte,

TRAPS

) {

Symbol* field = pool->name_ref_at(index);

Symbol* sig = pool->signature_ref_at(index);

// resolve specified klass 连接特定的类

KlassHandle resolved_klass;

resolve_klass(resolved_klass, pool, index, CHECK);

KlassHandle current_klass(THREAD, pool->pool_holder());

resolve_field(result, resolved_klass, field, sig, current_klass, byte, true, true, CHECK);

} 

从pool中查找到的index处的索引项为CONSTANT_NameAndType_info,格式如下:

CONSTANT_NameAndType_info {

u1 tag;

u2 name_index; // 占用16位

u2 descriptor_index; // 占用16位

}

常量池中的一个CONSTANT_NameAndType_info数据项, 可以看做CONSTANT_NameAndType类型的一个实例 。 从这个数据项的名称可以看出, 它描述了两种信息,第一种信息是名称(Name), 第二种信息是类型(Type) 。这里的名称是指方法的名称或者字段的名称, 而Type是广义上的类型,它其实描述的是字段的描述符或方法的描述符。 也就是说, 如果Name部分是一个字段名称,那么Type部分就是相应字段的描述符; 如果Name部分描述的是一个方法的名称,那么Type部分就是对应的方法的描述符。 也就是说,一个CONSTANT_NameAndType_info就表示了一个方法或一个字段。

调用resolve_klass()连接类,调用resolve_field()连接字段。在resolve_field()函数中有如下实现:

InstanceKlass* tmp = InstanceKlass::cast(resolved_klass());

KlassHandle sel_klass(THREAD, tmp->find_field(field, sig, &fd));

最重要的就是调用InstanceKlass的find_field()函数查找字段,将查找到的相关信息存储到fieldDescriptor类型的fd中。关于字段在InstanceKlass中的存储以及具体的布局在《深入剖析Java虚拟机:源码剖析与实例详解(基础卷)》中已经详细介绍过,这里不再介绍。

fieldDescriptor类及重要属性的定义如下:

class fieldDescriptor VALUE_OBJ_CLASS_SPEC {

private:

AccessFlags _access_flags;

int _index; // the field index

constantPoolHandle _cp;

...

}

其中的_access_flags可用来表示字段是否有volatile、final等关键字修饰,_index表示字段是存储在InstanceKlass中相应数组的第几个元组中。_cp表示定义当前字段的类的常量池。

通过调用resolve_klass()和resolve_field()函数后就可拿到这些信息,然后返回到InterpreterRuntime::resolve_get_put()函数继续查看实现逻辑:

  TosState state  = as_TosState(info.field_type());

Bytecodes::Code put_code = (Bytecodes::Code)0;

InstanceKlass* klass = InstanceKlass::cast(info.field_holder());

bool uninitialized_static = ( (bytecode == Bytecodes::_getstatic || bytecode == Bytecodes::_putstatic) &&

!klass->is_initialized() );

Bytecodes::Code get_code = (Bytecodes::Code)0;

if (!uninitialized_static) {

get_code = ((is_static) ? Bytecodes::_getstatic : Bytecodes::_getfield);

// 1、是putfield或putstatic指令

// 2、是getstatic或getfield指令并且不是获取final变量的值

if (is_put || !info.access_flags().is_final()) {

put_code = ((is_static) ? Bytecodes::_putstatic : Bytecodes::_putfield);

}

}

ConstantPoolCacheEntry* cpce = cache_entry(thread);

cpce->set_field(

get_code, // 设置的是_indices中的b1,当为getstatic或getfield时,则其中存储的是Opcode

put_code, // 设置的是_indices中的b2,当为setstatic或setfield时,则其中存储的是Opcode,所以get_code与put_code如果要连接了,其值不为0

info.field_holder(), // 设置的是_f1字段,表示字段的拥有者

info.index(), // field_index,设置的是flags

info.offset(), // field_offset,设置的是_f2字段,Offset (in words) of field from start of instanceOop / Klass*

state, // field_type,设置的是flags

info.access_flags().is_final(), // 设置的是flags

info.access_flags().is_volatile(), // 设置的是flags

pool->pool_holder()

);

通过info中的信息就可以得到字段的各种信息,然后填充ConstantPoolEntry信息,这样下次就不用对字段进行连接了,或者说不用从InstanceKlass中查找字段信息了,可直接从ConstantPoolCacheEntry中找到所有想得到的信息。 

上图在《深入剖析Java虚拟机:源码剖析与实例详解(基础卷)》一书中详细介绍过,通过我们解读getstatic字节码的解释执行过程,可以清楚的知道常量池缓存项的作用。对于getstatic来说,开始就会判断_indices中的高8位存储的是否为getstatic的操作码,如果不是,则表示没有连接,所以要调用InterpreterRuntime::resolve_get_put()函数进行连接操作。

在连接完成或已经连接完成时会继续执行如下汇编代码:

// 将ConstantPoolCacheEntry的索引存储么%edx

0x00007fffe101fdc2: movzwl 0x1(%r13),%edx

// 将ConstantPoolCache的首地址存储到%rcx

0x00007fffe101fdc7: mov -0x28(%rbp),%rcx

// 获取对应的ConstantPoolCacheEntry对应的索引

0x00007fffe101fdcb: shl $0x2,%edx

// --resolved --

// 获取[_indices,_f1,_f2,_flags]中的_f2,由于ConstantPoolCache占用16字节,而_indices

// 和_f2各占用8字节,所以_f2的偏移为32字节,也就是0x32

// _f2中保存的是字段在java.lang.Class实例中的字节偏移,通过此偏移就可获取此字段存储在

// java.lang.Class实例的值

0x00007fffe101fdce: mov 0x20(%rcx,%rdx,8),%rbx

// 获取[_indices,_f1,_f2,_flags]中的_flags

0x00007fffe101fdd3: mov 0x28(%rcx,%rdx,8),%eax

// 获取[_indices,_f1,_f2,_flags]中的_f1,_f1保存了字段拥有者,

// 也就是java.lang.Class对象

0x00007fffe101fdd7: mov 0x18(%rcx,%rdx,8),%rcx

// 从_f1中获取_java_mirror属性的值

0x00007fffe101fddc: mov 0x70(%rcx),%rcx

// 将_flags向右移动28位,剩下TosState

0x00007fffe101fde0: shr $0x1c,%eax

0x00007fffe101fde3: and $0xf,%eax

// 如果不相等,说明TosState的值不为0,则跳转到notByte

0x00007fffe101fde6: jne 0x00007fffe101fdf6

// btos

// btos的编号为0,代码执行到这里时,可能栈顶缓存要求是btos

// %rcx中存储的是_java_mirror,%rbx中存储的是_f2,由于静态变量存储在_java_mirror中,所以要获取

// 对应的首地址并压入栈中

0x00007fffe101fdec: movsbl (%rcx,%rbx,1),%eax

0x00007fffe101fdf0: push %rax

// 跳转到Done

0x00007fffe101fdf1: jmpq 0x00007fffe101ff0c

// -- notByte --

// %eax中存储的是TosState,如果不为atos,则跳转到notObj

0x00007fffe101fdf6: cmp $0x7,%eax

0x00007fffe101fdf9: jne 0x00007fffe101fe90

// atos

// %rcx中存储的是_java_mirror,%rbx中存储的是_f2,

// 所以要获取静态变量的首地址并压入栈内

0x00007fffe101fdff: mov (%rcx,%rbx,1),%eax

0x00007fffe101fe02: push %r10

0x00007fffe101fe04: cmp 0x163a8d45(%rip),%r12 # 0x00007ffff73c8b50

0x00007fffe101fe0b: je 0x00007fffe101fe88

0x00007fffe101fe11: mov %rsp,-0x28(%rsp)

0x00007fffe101fe16: sub $0x80,%rsp

0x00007fffe101fe1d: mov %rax,0x78(%rsp)

0x00007fffe101fe22: mov %rcx,0x70(%rsp)

0x00007fffe101fe27: mov %rdx,0x68(%rsp)

0x00007fffe101fe2c: mov %rbx,0x60(%rsp)

0x00007fffe101fe31: mov %rbp,0x50(%rsp)

0x00007fffe101fe36: mov %rsi,0x48(%rsp)

0x00007fffe101fe3b: mov %rdi,0x40(%rsp)

0x00007fffe101fe40: mov %r8,0x38(%rsp)

0x00007fffe101fe45: mov %r9,0x30(%rsp)

0x00007fffe101fe4a: mov %r10,0x28(%rsp)

0x00007fffe101fe4f: mov %r11,0x20(%rsp)

0x00007fffe101fe54: mov %r12,0x18(%rsp)

0x00007fffe101fe59: mov %r13,0x10(%rsp)

0x00007fffe101fe5e: mov %r14,0x8(%rsp)

0x00007fffe101fe63: mov %r15,(%rsp)

0x00007fffe101fe67: movabs $0x7ffff6d4d828,%rdi

0x00007fffe101fe71: movabs $0x7fffe101fe11,%rsi

0x00007fffe101fe7b: mov %rsp,%rdx

0x00007fffe101fe7e: and $0xfffffffffffffff0,%rsp

0x00007fffe101fe82: callq 0x00007ffff6872e3a

0x00007fffe101fe87: hlt

0x00007fffe101fe88: pop %r10

0x00007fffe101fe8a: push %rax

0x00007fffe101fe8b: jmpq 0x00007fffe101ff0c

// -- notObj --

0x00007fffe101fe90: cmp $0x3,%eax

// 如果不为itos,则跳转到notInt

0x00007fffe101fe93: jne 0x00007fffe101fea2

// itos

0x00007fffe101fe99: mov (%rcx,%rbx,1),%eax

0x00007fffe101fe9c: push %rax

// 跳转到Done

0x00007fffe101fe9d: jmpq 0x00007fffe101ff0c

// -- notInt --

// 如果不为ctos,则跳转到notChar

0x00007fffe101fea2: cmp $0x1,%eax

0x00007fffe101fea5: jne 0x00007fffe101feb5

// ctos

0x00007fffe101feab: movzwl (%rcx,%rbx,1),%eax

0x00007fffe101feaf: push %rax

// 跳转到Done

0x00007fffe101feb0: jmpq 0x00007fffe101ff0c

// -- notChar --

// 如果不为stos,则跳转到notShort

0x00007fffe101feb5: cmp $0x2,%eax

0x00007fffe101feb8: jne 0x00007fffe101fec8

// stos

0x00007fffe101febe: movswl (%rcx,%rbx,1),%eax

0x00007fffe101fec2: push %rax

// 跳转到done

0x00007fffe101fec3: jmpq 0x00007fffe101ff0c

// -- notShort --

// 如果不为ltos,则跳转到notLong

0x00007fffe101fec8: cmp $0x4,%eax

0x00007fffe101fecb: jne 0x00007fffe101fee2

// ltos

0x00007fffe101fed1: mov (%rcx,%rbx,1),%rax

0x00007fffe101fed5: sub $0x10,%rsp

0x00007fffe101fed9: mov %rax,(%rsp)

// 跳转到Done

0x00007fffe101fedd: jmpq 0x00007fffe101ff0c

// -- notLong --

// 如果不为ftos,则跳转到notFloat

0x00007fffe101fee2: cmp $0x5,%eax

0x00007fffe101fee5: jne 0x00007fffe101fefe

// ftos

0x00007fffe101feeb: vmovss (%rcx,%rbx,1),%xmm0

0x00007fffe101fef0: sub $0x8,%rsp

0x00007fffe101fef4: vmovss %xmm0,(%rsp)

// 跳转到Done

0x00007fffe101fef9: jmpq 0x00007fffe101ff0c

// -- notFloat --

0x00007fffe101fefe: vmovsd (%rcx,%rbx,1),%xmm0

0x00007fffe101ff03: sub $0x10,%rsp

0x00007fffe101ff07: vmovsd %xmm0,(%rsp)  

  

// -- Done --

  

如上汇编代码虽然多,但是完成的逻辑却非常简单,就是通过ConstantPoolCacheEntry中存储的信息(所谓的字节码连接完成指的就是对应的常量池缓存项的信息已经完善)完成压栈的逻辑。由于静态字段的值存储在java.lang.Class实例中,所以需要获取到对应的值,然后根据栈顶缓存要求的状态将值压入表达式栈即可。

第25篇-虚拟机对象操作指令之getfield

getfield指令表示获取指定类的实例域,并将其值压入栈顶。其格式如下:

getstatic indexbyte1 indexbyte2

无符号数indexbyte1和indexbyte2构建为(indexbyte1<<8)|indexbyte2,这个值指明了一个当前类的运行时常量池索引值,指向的运行时常量池项为一个字段的符号引用。

getfield字节码指令的生成函数为TemplateTable::getfield(),这些生成函数如下:

void TemplateTable::getfield(int byte_no) {

getfield_or_static(byte_no, false); // getfield的byte_no值为1

}

最终会调用getfield_or_static()函数生成机器指令片段。此函数生成的机器指令片段对应的汇编代码如下:

0x00007fffe10202d0: movzwl 0x1(%r13),%edx

0x00007fffe10202d5: mov -0x28(%rbp),%rcx

0x00007fffe10202d9: shl $0x2,%edx

0x00007fffe10202dc: mov 0x10(%rcx,%rdx,8),%ebx

0x00007fffe10202e0: shr $0x10,%ebx

0x00007fffe10202e3: and $0xff,%ebx

// 0xb4是getfield指令的Opcode,如果相等,则说明已经连接,直接跳转到resolved

0x00007fffe10202e9: cmp $0xb4,%ebx

0x00007fffe10202ef: je 0x00007fffe102038e

0x00007fffe10202f5: mov $0xb4,%ebx

// 省略通过调用MacroAssembler::call_VM()函数来执行

// InterpreterRuntime::resolve_get_put()函数的汇编代码

// ...

调用MacroAssembler::call_VM()函数生成如下代码,通过这些代码来执行InterpreterRuntime::resolve_get_put()函数。MacroAssembler::call_VM()函数的汇编在之前已经详细介绍过,这里不再介绍,直接给出汇编代码,如下:

0x00007fffe10202fa: callq  0x00007fffe1020304

0x00007fffe10202ff: jmpq 0x00007fffe1020382

0x00007fffe1020304: mov %rbx,%rsi

0x00007fffe1020307: lea 0x8(%rsp),%rax

0x00007fffe102030c: mov %r13,-0x38(%rbp)

0x00007fffe1020310: mov %r15,%rdi

0x00007fffe1020313: mov %rbp,0x200(%r15)

0x00007fffe102031a: mov %rax,0x1f0(%r15)

0x00007fffe1020321: test $0xf,%esp

0x00007fffe1020327: je 0x00007fffe102033f

0x00007fffe102032d: sub $0x8,%rsp

0x00007fffe1020331: callq 0x00007ffff66b567c

0x00007fffe1020336: add $0x8,%rsp

0x00007fffe102033a: jmpq 0x00007fffe1020344

0x00007fffe102033f: callq 0x00007ffff66b567c

0x00007fffe1020344: movabs $0x0,%r10

0x00007fffe102034e: mov %r10,0x1f0(%r15)

0x00007fffe1020355: movabs $0x0,%r10

0x00007fffe102035f: mov %r10,0x200(%r15)

0x00007fffe1020366: cmpq $0x0,0x8(%r15)

0x00007fffe102036e: je 0x00007fffe1020379

0x00007fffe1020374: jmpq 0x00007fffe1000420

0x00007fffe1020379: mov -0x38(%rbp),%r13

0x00007fffe102037d: mov -0x30(%rbp),%r14

0x00007fffe1020381: retq

如上代码完成的事情很简单,就是调用C++函数编写的InterpreterRuntime::resolve_get_put()函数,此函数会填充常量池缓存中ConstantPoolCacheEntry信息,关于ConstantPoolCache以及ConstantPoolCacheEntry,还有ConstantPoolCacheEntry中各个字段的含义在《深入剖析Java虚拟机:源码剖析与实例详解(基础卷)》中已经详细介绍过,这里不再介绍。

0x00007fffe1020382: movzwl 0x1(%r13),%edx

0x00007fffe1020387: mov -0x28(%rbp),%rcx

0x00007fffe102038b: shl $0x2,%edx

---- resolved ----

// 获取[_indices,_f1,_f2,_flags]中的_f2,由于ConstantPoolCache占用16字节,而_indices

// 和_f2各占用8字节,所以_f2的偏移为32字节,也就是0x32

// _f2中保存的是字段在oop实例中的字节偏移,通过此偏移就可获取此字段存储在

// oop中的值

0x00007fffe102038e: mov 0x20(%rcx,%rdx,8),%rbx

// 获取[_indices,_f1,_f2,_flags]中的_flags

0x00007fffe1020393: mov 0x28(%rcx,%rdx,8),%eax

// 将栈中的objectref对象弹出到%rcx中

0x00007fffe1020397: pop %rcx

// provoke(激起; 引起; 引发) OS NULL exception if reg = NULL by

// accessing M[reg] w/o changing any (non-CC) registers

// NOTE: cmpl is plenty(足够) here to provoke a segv

0x00007fffe1020398: cmp (%rcx),%rax

// 将_flags向右移动28位,剩下TosState

0x00007fffe102039b: shr $0x1c,%eax

0x00007fffe102039e: and $0xf,%eax

// 如果不相等,说明TosState的值不为0,则跳转到notByte

0x00007fffe10203a1: jne 0x00007fffe10203ba

// btos

// btos的编号为0,代码执行到这里时,可能栈顶缓存要求是btos

// %rcx中存储的是objectref,%rbx中存储的是_f2,获取字段对应的值存储到%rax中

0x00007fffe10203a7: movsbl (%rcx,%rbx,1),%eax

0x00007fffe10203ab: push %rax

// 对字节码指令进行重写,将Bytecodes::_fast_bgetfield的Opcode存储到%ecx中

0x00007fffe10203ac: mov $0xcc,%ecx

// 将Bytecodes::_fast_bgetfield的Opcode更新到字节码指令的操作码

0x00007fffe10203b1: mov %cl,0x0(%r13)

// 跳转到---- Done ----

0x00007fffe10203b5: jmpq 0x00007fffe102050f

---- notByte ----

0x00007fffe10203ba: cmp $0x7,%eax

0x00007fffe10203bd: jne 0x00007fffe102045d // 跳转到notObj

// atos

// 调用MacroAssembler::load_heap_oop()函数生成如下代码

0x00007fffe10203c3: mov (%rcx,%rbx,1),%eax

// ... 省略部分代码

// 结束MacroAssembler::load_heap_oop()函数的调用

0x00007fffe102044e: push %rax

// 重写字节码指令为Bytecodes::_fast_agetfield

0x00007fffe102044f: mov $0xcb,%ecx

0x00007fffe1020454: mov %cl,0x0(%r13)

0x00007fffe1020458: jmpq 0x00007fffe102050f

// -- notObj --

0x00007fffe102045d: cmp $0x3,%eax

0x00007fffe1020460: jne 0x00007fffe1020478 // 跳转到notInt

// itos

0x00007fffe1020466: mov (%rcx,%rbx,1),%eax

0x00007fffe1020469: push %rax

// 重写字节码指令o Bytecodes::_fast_igetfield

0x00007fffe102046a: mov $0xd0,%ecx

0x00007fffe102046f: mov %cl,0x0(%r13)

0x00007fffe1020473: jmpq 0x00007fffe102050f

// --- notInt ----

0x00007fffe1020478: cmp $0x1,%eax

0x00007fffe102047b: jne 0x00007fffe1020494 // 跳转到notChar

// ctos

0x00007fffe1020481: movzwl (%rcx,%rbx,1),%eax

0x00007fffe1020485: push %rax

// 重写字节码指令为Bytecodes::_fast_cgetfield

0x00007fffe1020486: mov $0xcd,%ecx

0x00007fffe102048b: mov %cl,0x0(%r13)

0x00007fffe102048f: jmpq 0x00007fffe102050f

// ---- notChar ----

0x00007fffe1020494: cmp $0x2,%eax

0x00007fffe1020497: jne 0x00007fffe10204b0 // 跳转到notShort

// stos

0x00007fffe102049d: movswl (%rcx,%rbx,1),%eax

0x00007fffe10204a1: push %rax

// 重写字节码指令为Bytecodes::_fast_sgetfield

0x00007fffe10204a2: mov $0xd2,%ecx

0x00007fffe10204a7: mov %cl,0x0(%r13)

0x00007fffe10204ab: jmpq 0x00007fffe102050f

// ---- notShort ----

0x00007fffe10204b0: cmp $0x4,%eax

0x00007fffe10204b3: jne 0x00007fffe10204d3 // 跳转到notLong

// ltos

0x00007fffe10204b9: mov (%rcx,%rbx,1),%rax

0x00007fffe10204bd: sub $0x10,%rsp

0x00007fffe10204c1: mov %rax,(%rsp)

// 重写字节码指令为Bytecodes::_fast_lgetfield,

0x00007fffe10204c5: mov $0xd1,%ecx

0x00007fffe10204ca: mov %cl,0x0(%r13)

0x00007fffe10204ce: jmpq 0x00007fffe102050f

// ---- notLong ----

0x00007fffe10204d3: cmp $0x5,%eax

0x00007fffe10204d6: jne 0x00007fffe10204f8 // 跳转到notFloat

// ftos

0x00007fffe10204dc: vmovss (%rcx,%rbx,1),%xmm0

0x00007fffe10204e1: sub $0x8,%rsp

0x00007fffe10204e5: vmovss %xmm0,(%rsp)

// 重写字节码指令为Bytecodes::_fast_fgetfield

0x00007fffe10204ea: mov $0xcf,%ecx

0x00007fffe10204ef: mov %cl,0x0(%r13)

0x00007fffe10204f3: jmpq 0x00007fffe102050f

// ---- notFloat ----

0x00007fffe10204f8: vmovsd (%rcx,%rbx,1),%xmm0

0x00007fffe10204fd: sub $0x10,%rsp

0x00007fffe1020501: vmovsd %xmm0,(%rsp)

0x00007fffe1020506: mov $0xce,%ecx

0x00007fffe102050b: mov %cl,0x0(%r13)

// -- Done --  

  

我们需要介绍一下虚拟机内部的一些自定义指令,这些自定义指令的模板如下:

// JVM bytecodes

def(Bytecodes::_fast_agetfield , ubcp|____|____|____, atos, atos, fast_accessfield , atos );

def(Bytecodes::_fast_bgetfield , ubcp|____|____|____, atos, itos, fast_accessfield , itos );

def(Bytecodes::_fast_cgetfield , ubcp|____|____|____, atos, itos, fast_accessfield , itos );

def(Bytecodes::_fast_dgetfield , ubcp|____|____|____, atos, dtos, fast_accessfield , dtos );

def(Bytecodes::_fast_fgetfield , ubcp|____|____|____, atos, ftos, fast_accessfield , ftos );

def(Bytecodes::_fast_igetfield , ubcp|____|____|____, atos, itos, fast_accessfield , itos );

def(Bytecodes::_fast_lgetfield , ubcp|____|____|____, atos, ltos, fast_accessfield , ltos );

def(Bytecodes::_fast_sgetfield , ubcp|____|____|____, atos, itos, fast_accessfield , itos );

以_fast_agetfield内部定义的字节码指令为例为来,生成函数为TemplateTable::fast_accessfield()函数,汇编代码如下:

0x00007fffe101e4e1: movzwl 0x1(%r13),%ebx

0x00007fffe101e4e6: mov -0x28(%rbp),%rcx

0x00007fffe101e4ea: shl $0x2,%ebx

// 计算%rcx+%rdx*8+0x20,获取ConstantPoolCacheEntry[_indices,_f1,_f2,_flags]中的_f2

// 因为ConstantPoolCache的大小为0x16字节,%rcx+0x20定位到第一个ConstantPoolCacheEntry的开始位置

// %rdx*8算出来的是相对于第一个ConstantPoolCacheEntry的字节偏移

0x00007fffe101e4ed: mov 0x20(%rcx,%rbx,8),%rbx

// 检查空异常

0x00007fffe101e4f2: cmp (%rax),%rax

// %rax中存储的是objectref,也就是要从这个实例中获取字段的值,通过偏移%rbx后就

// 能获取到偏移的值,然后加载到%eax

0x00007fffe101e4f5: mov (%rax,%rbx,1),%eax

  

其它的字节码指令类似,这里不再过多介绍。从这里可以看出,我们不需要再执行getfield对应的那些汇编指令,只执行_fast开头的指令即可,这些指令比起getfield指令来说简化了很多,大大提高了解释执行的速度。  

第26篇-虚拟机对象操作指令之putstatic

之前已经介绍了getstatic与getfield指令的汇编代码执行逻辑,这一篇介绍putstatic指令的执行逻辑,putfield将不再介绍,大家可以自己去研究,相信大家有这个实力。

putstatic指令为指定类的静态域赋值。字节码指令的格式如下:

putstatic indexbyte1 indexbyte2

无符号数indexbyte1和indexbyte2构建为(indexbyte1<<8)|indexbyte2,该索引所指向的运行时常量池项应当是一个字段的符号引用。

指令的模板定义如下:

def(Bytecodes::_putstatic           , ubcp|____|clvm|____, vtos, vtos, putstatic           , f2_byte      );  

生成函数为putstatic(),函数的实现如下:

void TemplateTable::putstatic(int byte_no) {

putfield_or_static(byte_no, false);

}

调用TemplateTable::putfield_or_static()函数生成的机器指令对应的汇编代码如下:

0x00007fffe101ff90: movzwl 0x1(%r13),%edx

0x00007fffe101ff95: mov -0x28(%rbp),%rcx

0x00007fffe101ff99: shl $0x2,%edx

0x00007fffe101ff9c: mov 0x10(%rcx,%rdx,8),%ebx

0x00007fffe101ffa0: shr $0x18,%ebx

0x00007fffe101ffa3: and $0xff,%ebx

// 是否已经对putstatic指令进行了连接,如果已经连接,则跳转到resolved

0x00007fffe101ffa9: cmp $0xb3,%ebx

0x00007fffe101ffaf: je 0x00007fffe102004e

调用TemplateTable::resolve_cache_and_index()函数生成如下汇编代码:

// 执行到这里,说明字段还没有连接

0x00007fffe101ffb5: mov $0xb3,%ebx

// 调用MacroAssembler::call_VM()函数生成如下代码,

// 用来执行InterpreterRuntime::resolve_get_put()函数

0x00007fffe101ffba: callq 0x00007fffe101ffc4

0x00007fffe101ffbf: jmpq 0x00007fffe1020042

0x00007fffe101ffc4: mov %rbx,%rsi

0x00007fffe101ffc7: lea 0x8(%rsp),%rax

0x00007fffe101ffcc: mov %r13,-0x38(%rbp)

0x00007fffe101ffd0: mov %r15,%rdi

0x00007fffe101ffd3: mov %rbp,0x200(%r15)

0x00007fffe101ffda: mov %rax,0x1f0(%r15)

0x00007fffe101ffe1: test $0xf,%esp

0x00007fffe101ffe7: je 0x00007fffe101ffff

0x00007fffe101ffed: sub $0x8,%rsp

0x00007fffe101fff1: callq 0x00007ffff66b567c

0x00007fffe101fff6: add $0x8,%rsp

0x00007fffe101fffa: jmpq 0x00007fffe1020004

0x00007fffe101ffff: callq 0x00007ffff66b567c

0x00007fffe1020004: movabs $0x0,%r10

0x00007fffe102000e: mov %r10,0x1f0(%r15)

0x00007fffe1020015: movabs $0x0,%r10

0x00007fffe102001f: mov %r10,0x200(%r15)

0x00007fffe1020026: cmpq $0x0,0x8(%r15)

0x00007fffe102002e: je 0x00007fffe1020039

0x00007fffe1020034: jmpq 0x00007fffe1000420

0x00007fffe1020039: mov -0x38(%rbp),%r13

0x00007fffe102003d: mov -0x30(%rbp),%r14

0x00007fffe1020041: retq

0x00007fffe1020042: movzwl 0x1(%r13),%edx

0x00007fffe1020047: mov -0x28(%rbp),%rcx

0x00007fffe102004b: shl $0x2,%edx

接下来生成的汇编代码如下:

// ---- resolved ----

// 执行如下代码时,表示字段已经连接完成

0x00007fffe102004e: mov 0x20(%rcx,%rdx,8),%rbx

0x00007fffe1020053: mov 0x28(%rcx,%rdx,8),%eax

0x00007fffe1020057: mov 0x18(%rcx,%rdx,8),%rcx

0x00007fffe102005c: mov 0x70(%rcx),%rcx

0x00007fffe1020060: mov %eax,%edx

// 将_flags向右移动21位,判断是否有volatile关键字

0x00007fffe1020062: shr $0x15,%edx

0x00007fffe1020065: and $0x1,%edx

// 将_flags向右移动28位,剩下TosState

0x00007fffe1020068: shr $0x1c,%eax

// 如果不为btos,则跳转到notByte

0x00007fffe102006b: and $0xf,%eax

0x00007fffe102006e: jne 0x00007fffe1020083

// btos

// 将栈顶的值存储到%eax中,这个值会写入到对应的字段中

0x00007fffe1020074: mov (%rsp),%eax

0x00007fffe1020077: add $0x8,%rsp

// %rcx为_java_mirror,%rbx为_f2,表示域在类中的偏移

0x00007fffe102007b: mov %al,(%rcx,%rbx,1)

0x00007fffe102007e: jmpq 0x00007fffe10201be // 跳转到Done

// -- notByte --

// 如果不为atos,则跳转到notObj

0x00007fffe1020083: cmp $0x7,%eax

0x00007fffe1020086: jne 0x00007fffe1020130

// atos

// 将栈顶的值弹出到%rax中,这个值将用来更新对应字段的值

0x00007fffe102008c: pop %rax

// ...

// 将值更新到对应的字段上

0x00007fffe1020115: mov %eax,(%rcx,%rbx,1)

// 其中的0x9是CardTableModRefBS::card_shift,shr表示逻辑右移,由于%rcx指向的是

// java.lang.Class实例的首地址,向右移后%rcx就算出了卡表的索引

0x00007fffe1020118: shr $0x9,%rcx

// 地址常量$0x7fffe07ff000表示卡表的基地址

0x00007fffe102011c: movabs $0x7fffe07ff000,%r10

// 将对应的卡表项标记为脏,其中常量0x0就表示是脏卡

0x00007fffe1020126: movb $0x0,(%r10,%rcx,1)

0x00007fffe102012b: jmpq

0x00007fffe10201be // 跳转到Done

// ---- notObj ----

// 如果不为itos,那么跳转到notInt

0x00007fffe1020130: cmp $0x3,%eax

0x00007fffe1020133: jne 0x00007fffe1020148

// itos

0x00007fffe1020139: mov (%rsp),%eax

// 如果不为ctos,则跳转到notChar

0x00007fffe102013c: add $0x8,%rsp

0x00007fffe1020140: mov %eax,(%rcx,%rbx,1)

0x00007fffe1020143: jmpq 0x00007fffe10201be // 跳转到Done

0x00007fffe1020148: cmp $0x1,%eax

0x00007fffe102014b: jne 0x00007fffe1020161

// ctos

0x00007fffe1020151: mov (%rsp),%eax

0x00007fffe1020154: add $0x8,%rsp

0x00007fffe1020158: mov %ax,(%rcx,%rbx,1)

0x00007fffe102015c: jmpq 0x00007fffe10201be // 跳转到Done

0x00007fffe1020161: cmp $0x2,%eax

0x00007fffe1020164: jne 0x00007fffe102017a

// stos

0x00007fffe102016a: mov (%rsp),%eax

0x00007fffe102016d: add $0x8,%rsp

0x00007fffe1020171: mov %ax,(%rcx,%rbx,1)

0x00007fffe1020175: jmpq 0x00007fffe10201be // 跳转到Done

0x00007fffe102017a: cmp $0x4,%eax

0x00007fffe102017d: jne 0x00007fffe1020194

// ltos

0x00007fffe1020183: mov (%rsp),%rax

0x00007fffe1020187: add $0x10,%rsp

0x00007fffe102018b: mov %rax,(%rcx,%rbx,1)

0x00007fffe102018f: jmpq 0x00007fffe10201be // 跳转到Done

0x00007fffe1020194: cmp $0x5,%eax

0x00007fffe1020197: jne 0x00007fffe10201b0

// ftos

0x00007fffe102019d: vmovss (%rsp),%xmm0

0x00007fffe10201a2: add $0x8,%rsp

0x00007fffe10201a6: vmovss %xmm0,(%rcx,%rbx,1)

0x00007fffe10201ab: jmpq 0x00007fffe10201be // 跳转到Done

// dtos

0x00007fffe10201b0: vmovsd (%rsp),%xmm0

0x00007fffe10201b5: add $0x10,%rsp

0x00007fffe10201b9: vmovsd %xmm0,(%rcx,%rbx,1)

// ---- Done ----

0x00007fffe10201be: test %edx,%edx

0x00007fffe10201c0: je 0x00007fffe10201cb

0x00007fffe10201c6: lock addl $0x0,(%rsp)

// ---- notVolatile ---- 

  

在如上代码中,最值得关注的2个点如下:

(1)更新引用字段时,通过屏障将对应的卡表项标记为脏,这样可在GC过程中扫描脏卡就可将活跃对象标记出来而不会造成遗漏;

(2)当字段有volatile关键字修饰时,需要填写lock指令前缀,这个前缀在之前介绍x86-64机器指令时没有介绍过,这里摘抄一下别人对此指令的介绍:

Intel手册对 lock 前缀的说明如下:

  1. 确保被修饰指令执行的原子性;
  2. 禁止该指令与前面和后面的读写指令重排序;
  3. 指令执行完后把写缓冲区的所有数据刷新到内存中(这样这个指令之前的其他修改对所有处理器可见)。

在所有的 X86 CPU 上都具有锁定一个特定内存地址的能力,当这个特定内存地址被锁定后,它就可以阻止其他的系统总线读取或修改这个内存地址。这种能力是通过 lock 指令前缀再加上下面的汇编指令来实现的。当使用 lock 指令前缀时,它会使 CPU 宣告一个 lock# 信号,这样就能确保在多处理器系统或多线程竞争的环境下互斥地使用这个内存地址。当指令执行完毕,这个锁定动作也就会消失。

第27篇-虚拟机字节码指令之操作数栈管理指令

操作数栈管理相关的字节码指令如下表所示。

0x57pop将栈顶数值弹出 (数值不能是long或double类型的)
0x58pop2将栈顶的一个(long或double类型的)或两个数值弹出(其它)
0x59dup复制栈顶数值并将复制值压入栈顶
0x5adup_x1复制栈顶数值并将两个复制值压入栈顶
0x5bdup_x2复制栈顶数值并将三个(或两个)复制值压入栈顶
0x5cdup2复制栈顶一个(long或double类型的)或两个(其它)数值并将复制值压入栈顶
0x5ddup2_x1dup_x1 指令的双倍版本
0x5edup2_x2dup_x2 指令的双倍版本
0x5fswap将栈最顶端的两个数值互换(数值不能是long或double类型的)

字节码指令对应的模板定义如下:

def(Bytecodes::_pop         , ____|____|____|____, vtos, vtos, pop         ,  _           );

def(Bytecodes::_pop2 , ____|____|____|____, vtos, vtos, pop2 , _ );

def(Bytecodes::_dup , ____|____|____|____, vtos, vtos, dup , _ );

def(Bytecodes::_dup_x1 , ____|____|____|____, vtos, vtos, dup_x1 , _ );

def(Bytecodes::_dup_x2 , ____|____|____|____, vtos, vtos, dup_x2 , _ );

def(Bytecodes::_dup2 , ____|____|____|____, vtos, vtos, dup2 , _ );

def(Bytecodes::_dup2_x1 , ____|____|____|____, vtos, vtos, dup2_x1 , _ );

def(Bytecodes::_dup2_x2 , ____|____|____|____, vtos, vtos, dup2_x2 , _ );

def(Bytecodes::_swap , ____|____|____|____, vtos, vtos, swap , _ );

pop指令将栈顶数值弹出。对应的汇编代码如下:

add    $0x8,%rsp   

pop2指令将栈顶数值弹出。对应的汇编代码如下:

add    $0x10,%rsp  

dup指令复制栈顶数值并将复制值压入栈顶。对应的汇编代码如下:

mov    (%rsp),%rax

push %rax

swap指令将栈最顶端的两个数值互换(数值不能是long或double类型的)。对应的汇编代码如下:

mov    0x8(%rsp),%rcx

mov (%rsp),%rax

mov %rcx,(%rsp)

mov %rax,0x8(%rsp)

指令的执行逻辑比较简单,这里不再过多介绍。

第28篇-虚拟机字节码指令之控制转移指令

控制转移相关的字节码指令如下表所示。

0x99ifeq当栈顶int型数值等于0时跳转
0x9aifne当栈顶int型数值不等于0时跳转
0x9biflt当栈顶int型数值小于0时跳转
0x9cifge当栈顶int型数值大于等于0时跳转
0x9difgt当栈顶int型数值大于0时跳转
0x9eifle当栈顶int型数值小于等于0时跳转
0x9fif_icmpeq比较栈顶两int型数值大小,当结果等于0时跳转
0xa0if_icmpne比较栈顶两int型数值大小,当结果不等于0时跳转
0xa1if_icmplt比较栈顶两int型数值大小,当结果小于0时跳转
0xa2if_icmpge比较栈顶两int型数值大小,当结果大于等于0时跳转
0xa3if_icmpgt比较栈顶两int型数值大小,当结果大于0时跳转
0xa4if_icmple比较栈顶两int型数值大小,当结果小于等于0时跳转
0xa5if_acmpeq比较栈顶两引用型数值,当结果相等时跳转
0xa6if_acmpne比较栈顶两引用型数值,当结果不相等时跳转
0xa7goto无条件跳转
0xa8jsr跳转至指定16位offset位置,并将jsr下一条指令地址压入栈顶
0xa9ret返回至本地变量指令的index的指令位置(一般与jsr或jsr_w联合使用)
0xaatableswitch用于switch条件跳转,case值连续(可变长度指令)
0xablookupswitch用于switch条件跳转,case值不连续(可变长度指令)
0xacireturn从当前方法返回int
0xadlreturn从当前方法返回long
0xaefreturn从当前方法返回float
0xafdreturn从当前方法返回double
0xb0areturn从当前方法返回对象引用
0xb1return从当前方法返回void
0xc6ifnull为null时跳转
0xc7ifnonnull不为null时跳转
0xc8goto_w无条件跳转(宽索引)
0xc9jsr_w跳转至指定32位offset位置,并将jsr_w下一条指令地址压入栈顶

模板定义如下:

def(Bytecodes::_ifeq                , ubcp|____|clvm|____, itos, vtos, if_0cmp             , equal        );

def(Bytecodes::_ifne , ubcp|____|clvm|____, itos, vtos, if_0cmp , not_equal );

def(Bytecodes::_iflt , ubcp|____|clvm|____, itos, vtos, if_0cmp , less );

def(Bytecodes::_ifge , ubcp|____|clvm|____, itos, vtos, if_0cmp , greater_equal);

def(Bytecodes::_ifgt , ubcp|____|clvm|____, itos, vtos, if_0cmp , greater );

def(Bytecodes::_ifle , ubcp|____|clvm|____, itos, vtos, if_0cmp , less_equal );

def(Bytecodes::_if_icmpeq , ubcp|____|clvm|____, itos, vtos, if_icmp , equal );

def(Bytecodes::_if_icmpne , ubcp|____|clvm|____, itos, vtos, if_icmp , not_equal );

def(Bytecodes::_if_icmplt , ubcp|____|clvm|____, itos, vtos, if_icmp , less );

def(Bytecodes::_if_icmpge , ubcp|____|clvm|____, itos, vtos, if_icmp , greater_equal);

def(Bytecodes::_if_icmpgt , ubcp|____|clvm|____, itos, vtos, if_icmp , greater );

def(Bytecodes::_if_icmple , ubcp|____|clvm|____, itos, vtos, if_icmp , less_equal );

def(Bytecodes::_if_acmpeq , ubcp|____|clvm|____, atos, vtos, if_acmp , equal );

def(Bytecodes::_if_acmpne , ubcp|____|clvm|____, atos, vtos, if_acmp , not_equal );

def(Bytecodes::_goto , ubcp|disp|clvm|____, vtos, vtos, _goto , _ );

def(Bytecodes::_jsr , ubcp|disp|____|____, vtos, vtos, jsr , _ ); // result is not an oop, so do not transition to atos

def(Bytecodes::_ret , ubcp|disp|____|____, vtos, vtos, ret , _ );

def(Bytecodes::_tableswitch , ubcp|disp|____|____, itos, vtos, tableswitch , _ );

def(Bytecodes::_lookupswitch , ubcp|disp|____|____, itos, itos, lookupswitch , _ );

def(Bytecodes::_ireturn , ____|disp|clvm|____, itos, itos, _return , itos );

def(Bytecodes::_lreturn , ____|disp|clvm|____, ltos, ltos, _return , ltos );

def(Bytecodes::_freturn , ____|disp|clvm|____, ftos, ftos, _return , ftos );

def(Bytecodes::_dreturn , ____|disp|clvm|____, dtos, dtos, _return , dtos );

def(Bytecodes::_areturn , ____|disp|clvm|____, atos, atos, _return , atos );

def(Bytecodes::_return , ____|disp|clvm|____, vtos, vtos, _return , vtos );

def(Bytecodes::_ifnull , ubcp|____|clvm|____, atos, vtos, if_nullcmp , equal );

def(Bytecodes::_ifnonnull , ubcp|____|clvm|____, atos, vtos, if_nullcmp , not_equal );

def(Bytecodes::_goto_w , ubcp|____|clvm|____, vtos, vtos, goto_w , _ );

def(Bytecodes::_jsr_w , ubcp|____|____|____, vtos, vtos, jsr_w , _ );

下面介绍几个典型指令的汇编实现。

1、goto指令

goto字节码指令的生成函数为TemplateTable::_goto(),生成的汇编代码如下:(在生成代码时添加命令-Xint   -XX:-ProfileInterpreter,这样可排除生成一些不必要的指令)

// Method*保存到%rcx中

0x00007fffe1019df0: mov -0x18(%rbp),%rcx

// 将goto后的index(2个字节)存储到%edx中

0x00007fffe1019df4: movswl 0x1(%r13),%edx

0x00007fffe1019df9: bswap %edx

// 算术右移指令

0x00007fffe1019dfb: sar $0x10,%edx

// 将一个双字符号扩展后送到一个四字地址中

0x00007fffe1019dfe: movslq %edx,%rdx

// 将当前字节码地址加上rdx保存的偏移量,计算跳转的目标地址

0x00007fffe1019e01: add %rdx,%r13

// %r13已经变成目标跳转地址,这里是加载跳转地址的第一个字节码到rbx中

0x00007fffe1019e04: movzbl 0x0(%r13),%ebx

// continue with the bytecode @ target

// eax: return bci for jsr\'s, unused otherwise

// ebx: target bytecode

// r13: target bcp

// 开始执行跳转地址处的字节码,其中的常量地址为

// TemplateInterpreter::_active_table的、栈顶缓存状态为vtos的首地址

0x00007fffe1019e09: movabs $0x7ffff73ba4a0,%r10

0x00007fffe1019e13: jmpq *(%r10,%rbx,8)

其实goto指令实际上生成的汇编代码要比上面的代码多的多,因为goto指令是一个分支指令,其中会做一些性能统计以辅助进行编译优化,而且goto如果是在循环中的话,还可能会涉及到栈上替换的技术,所以后面我们在介绍到对应的技术点时再详细介绍goto指令的其它一些汇编逻辑。

2、ifeq、ifne等指令

现在ifeq、ifne等指令的生成函数为TemplateTable::if_0cmp()。ifeq字节码指令表示栈顶值与零值比较,当且仅当栈顶的int类型的值为0时,比较结果为真。对应的汇编代码如下: 

0x00007fffe10196c7: test   %eax,%eax

// 当栈顶缓存%eax不为0时,直接跳到not_taken

0x00007fffe10196c9: jne 0x00007fffe10196f6

// 调用TemplateTable::branch(false,false)函数生成的汇编代码

// 将当前栈帧中保存的Method* 拷贝到rcx中

0x00007fffe10196cf: mov -0x18(%rbp),%rcx

// 将当前字节码位置往后偏移1字节处开始的2字节数据读取到edx中

0x00007fffe10196d3: movswl 0x1(%r13),%edx

// 将%edx中的值字节次序变反

0x00007fffe10196d8: bswap %edx

// 将edx中的值右移16位,上述两步就是为了计算跳转分支的偏移量

0x00007fffe10196da: sar $0x10,%edx

// 将edx中的数据从2字节扩展成4字节

0x00007fffe10196dd: movslq %edx,%rdx

// 将当前字节码地址加上rdx保存的偏移量,计算跳转的目标地址

0x00007fffe10196e0: add %rdx,%r13

// r13已经变成目标跳转地址,这里是加载跳转地址的第一个字节码到ebx中

0x00007fffe10196e3: movzbl 0x0(%r13),%ebx

// 开始执行跳转地址处的字节码,其中的常量地址为

// TemplateInterpreter::_active_table的、栈顶缓存状态为vtos的首地址

0x00007fffe10196e8: movabs $0x7ffff73ba4a0,%r10

0x00007fffe10196f2: jmpq *(%r10,%rbx,8)

// -- not_taken -- 

类似的指令实现逻辑也高度类似,大家有兴趣可自行研究。

3、lookupswitch、tableswitch等指令

lookupswitch指令根据键值在跳转表中寻找配对的分支并跳转,具体的格式如下图所示。

这是一条变长指令并且要求所有的操作数都4字节对齐,所以紧跟在lookupswitch指令之后可能会有0到3个字节作为空白填充,而后面的default、npairs等都用4字节来表示,从当前方法开始(第一条字节码指令)计算的地址,即紧随空白填充的是一系列32位有符号整数值,包括默认跳转地址default、匹配坐标的数量npairs以及npairs组匹配坐标。其中npairs的值应当大于或等于0,每一组匹配坐标都包含了一个整数值match以及一个有符号32位偏移量offset。上述所有的32位有符号数值都是通过以下方式计算得到:

(byte1<<24)|(byte2<<24)|(byte3<<24)|byte4

tableswitch指令根据键值在跳转表中寻找配对的分支并跳转,具体的格式如下图所示。

这是一条变长指令并且要求所有的操作数都4字节对齐,所以紧跟在lookupswitch指令之后可能会有0到3个字节作为空白填充,而后面的default、lowbyte、highbyte等用4字节来表示,从当前方法开始(第一条字节码指令)计算的地址,即紧随空白填充的是一系列32位有符号整数值,包括默认跳转地址default、高位值high以及低位值low,在此之后是high-low+1个有符号32位偏移offset。上述所有的32位有符号数值都是通过以下方式计算得到:

(byte1<<24)|(byte2<<24)|(byte3<<24)|byte4

生成函数为TemplateTable::tableswitch(),生成的汇编如下:

// align r13,按照4字节对齐

0x00007fffe1019fa7: lea 0x4(%r13),%rbx

0x00007fffe1019fab: and $0xfffffffffffffffc,%rbx

// load lo & hi

0x00007fffe1019faf: mov 0x4(%rbx),%ecx

0x00007fffe1019fb2: mov 0x8(%rbx),%edx

0x00007fffe1019fb5: bswap %ecx

0x00007fffe1019fb7: bswap %edx

// check against lo & hi

// %ecx中存储的是lowbyte

0x00007fffe1019fb9: cmp %ecx,%eax

// 如果比低位值还低,则跳转到default_case

0x00007fffe1019fbb: jl 0x00007fffe1019feb

// %edx中存储的是highbyte

0x00007fffe1019fc1: cmp %edx,%eax

// 如果比高位值还高,则跳转到default_case

0x00007fffe1019fc3: jg 0x00007fffe1019feb

// lookup dispatch offset

0x00007fffe1019fc9: sub %ecx,%eax

// %rbx中存储的是对齐后的字节码指令地址,%rax中存储的是栈顶缓存值

0x00007fffe1019fcb: mov 0xc(%rbx,%rax,4),%edx

// -- continue_execution --

// continue execution

0x00007fffe1019fcf: bswap %edx

0x00007fffe1019fd1: movslq %edx,%rdx

0x00007fffe1019fd4: movzbl 0x0(%r13,%rdx,1),%ebx

0x00007fffe1019fda: add %rdx,%r13

0x00007fffe1019fdd: movabs $0x7ffff73ba4a0,%r10

0x00007fffe1019fe7: jmpq *(%r10,%rbx,8)

// -- default_case --

// handle default

0x00007fffe1019feb: mov (%rbx),%edx

// 跳转到continue_execution

0x00007fffe1019fed: jmp 0x00007fffe1019fcf

以上是 【JVM源码解析】模板解释器解释执行Java字节码指令(下) 的全部内容, 来源链接: utcz.com/z/392841.html

回到顶部