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

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

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

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

0x60 iadd 将栈顶两int型数值相加并将结果压入栈顶
0x61 ladd 将栈顶两long型数值相加并将结果压入栈顶
0x62 fadd 将栈顶两float型数值相加并将结果压入栈顶
0x63 dadd 将栈顶两double型数值相加并将结果压入栈顶
0x64 isub 将栈顶两int型数值相减并将结果压入栈顶
0x65 lsub 将栈顶两long型数值相减并将结果压入栈顶
0x66 fsub 将栈顶两float型数值相减并将结果压入栈顶
0x67 dsub 将栈顶两double型数值相减并将结果压入栈顶
0x68 imul 将栈顶两int型数值相乘并将结果压入栈顶
0x69 lmul 将栈顶两long型数值相乘并将结果压入栈顶
0x6a fmul 将栈顶两float型数值相乘并将结果压入栈顶
0x6b dmul 将栈顶两double型数值相乘并将结果压入栈顶
0x6c idiv 将栈顶两int型数值相除并将结果压入栈顶
0x6d ldiv 将栈顶两long型数值相除并将结果压入栈顶
0x6e fdiv 将栈顶两float型数值相除并将结果压入栈顶
0x6f ddiv 将栈顶两double型数值相除并将结果压入栈顶
0x70 irem 将栈顶两int型数值作取模运算并将结果压入栈顶
0x71 lrem 将栈顶两long型数值作取模运算并将结果压入栈顶
0x72 frem 将栈顶两float型数值作取模运算并将结果压入栈顶
0x73 drem 将栈顶两double型数值作取模运算并将结果压入栈顶
0x74 ineg 将栈顶int型数值取负并将结果压入栈顶
0x75 lneg 将栈顶long型数值取负并将结果压入栈顶
0x76 fneg 将栈顶float型数值取负并将结果压入栈顶
0x77 dneg 将栈顶double型数值取负并将结果压入栈顶
0x78 ishl 将int型数值左移位指定位数并将结果压入栈顶
0x79 lshl 将long型数值左移位指定位数并将结果压入栈顶
0x7a ishr 将int型数值右(符号)移位指定位数并将结果压入栈顶
0x7b lshr 将long型数值右(符号)移位指定位数并将结果压入栈顶
0x7c iushr 将int型数值右(无符号)移位指定位数并将结果压入栈顶
0x7d lushr 将long型数值右(无符号)移位指定位数并将结果压入栈顶
0x7e iand 将栈顶两int型数值作“按位与”并将结果压入栈顶
0x7f land 将栈顶两long型数值作“按位与”并将结果压入栈顶
0x80 ior 将栈顶两int型数值作“按位或”并将结果压入栈顶
0x81 lor 将栈顶两long型数值作“按位或”并将结果压入栈顶
0x82 ixor 将栈顶两int型数值作“按位异或”并将结果压入栈顶
0x83 lxor 将栈顶两long型数值作“按位异或”并将结果压入栈顶
0x84 iinc 将指定int型变量增加指定值(i++、i--、i+=2)
0x94 lcmp 比较栈顶两long型数值大小,并将结果(1、0或-1)压入栈顶
0x95 fcmpl 比较栈顶两float型数值大小,并将结果(1、0或-1)压入栈顶;当其中一个数值为NaN时,将-1压入栈顶
0x96 fcmpg 比较栈顶两float型数值大小,并将结果(1、0或-1)压入栈顶;当其中一个数值为NaN时,将1压入栈顶
0x97 dcmpl 比较栈顶两double型数值大小,并将结果(1、0或-1)压入栈顶;当其中一个数值为NaN时,将-1压入栈顶
0x98 dcmpg 比较栈顶两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函数会使用规定的寄存器,如下图所示。

【JVM源码解析】模板解释器解释执行Java字节码指令(下)_第1张图片

汇编对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虚拟机规范中定义的类型转换相关的字节码指令如下表所示。

0x85 i2l 将栈顶int型数值强制转换成long型数值并将结果压入栈顶
0x86 i2f 将栈顶int型数值强制转换成float型数值并将结果压入栈顶
0x87 i2d 将栈顶int型数值强制转换成double型数值并将结果压入栈顶
0x88 l2i 将栈顶long型数值强制转换成int型数值并将结果压入栈顶
0x89 l2f 将栈顶long型数值强制转换成float型数值并将结果压入栈顶
0x8a l2d 将栈顶long型数值强制转换成double型数值并将结果压入栈顶
0x8b f2i 将栈顶float型数值强制转换成int型数值并将结果压入栈顶
0x8c f2l 将栈顶float型数值强制转换成long型数值并将结果压入栈顶
0x8d f2d 将栈顶float型数值强制转换成double型数值并将结果压入栈顶
0x8e d2i 将栈顶double型数值强制转换成int型数值并将结果压入栈顶
0x8f d2l 将栈顶double型数值强制转换成long型数值并将结果压入栈顶
0x90 d2f 将栈顶double型数值强制转换成float型数值并将结果压入栈顶
0x91 i2b 将栈顶int型数值强制转换成byte型数值并将结果压入栈顶
0x92 i2c 将栈顶int型数值强制转换成char型数值并将结果压入栈顶
0x93 i2s 将栈顶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虚拟机规范中定义的对象操作相关的字节码指令如下表所示。 

0xb2 getstatic 获取指定类的静态域,并将其值压入栈顶
0xb3 putstatic 为指定的类的静态域赋值
0xb4 getfield 获取指定类的实例域,并将其值压入栈顶
0xb5 putfield 为指定的类的实例域赋值
0xbb new 创建一个对象,并将其引用值压入栈顶
0xbc newarray 创建一个指定原始类型(如int,、float,、char等)的数组,并将其引用值压入栈顶
0xbd anewarray 创建一个引用型(如类、接口或数组)的数组,并将其引用值压入栈顶
0xbe arraylength 获得数组的长度值并压入栈顶
0xc0 checkcast 检验类型转换,检验未通过将抛出ClassCastException
0xc1 instanceof 检验对象是否是指定的类的实例,如果是将1压入栈顶,否则将0压入栈顶
0xc5 multianewarray 创建指定类型和指定维度的多维数组(执行该指令时,操作栈中必须包含各维度的长度值),并将其引用值压入栈顶

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

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中找到所有想得到的信息。 

 【JVM源码解析】模板解释器解释执行Java字节码指令(下)_第2张图片 

上图在《深入剖析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篇-虚拟机字节码指令之操作数栈管理指令

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

0x57 pop 将栈顶数值弹出 (数值不能是long或double类型的)
0x58 pop2 将栈顶的一个(long或double类型的)或两个数值弹出(其它)
0x59 dup 复制栈顶数值并将复制值压入栈顶
0x5a dup_x1 复制栈顶数值并将两个复制值压入栈顶
0x5b dup_x2 复制栈顶数值并将三个(或两个)复制值压入栈顶
0x5c dup2 复制栈顶一个(long或double类型的)或两个(其它)数值并将复制值压入栈顶
0x5d dup2_x1 dup_x1 指令的双倍版本
0x5e dup2_x2 dup_x2 指令的双倍版本
0x5f swap 将栈最顶端的两个数值互换(数值不能是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篇-虚拟机字节码指令之控制转移指令

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

0x99 ifeq 当栈顶int型数值等于0时跳转
0x9a ifne 当栈顶int型数值不等于0时跳转
0x9b iflt 当栈顶int型数值小于0时跳转
0x9c ifge 当栈顶int型数值大于等于0时跳转
0x9d ifgt 当栈顶int型数值大于0时跳转
0x9e ifle 当栈顶int型数值小于等于0时跳转
0x9f if_icmpeq 比较栈顶两int型数值大小,当结果等于0时跳转
0xa0 if_icmpne 比较栈顶两int型数值大小,当结果不等于0时跳转
0xa1 if_icmplt 比较栈顶两int型数值大小,当结果小于0时跳转
0xa2 if_icmpge 比较栈顶两int型数值大小,当结果大于等于0时跳转
0xa3 if_icmpgt 比较栈顶两int型数值大小,当结果大于0时跳转
0xa4 if_icmple 比较栈顶两int型数值大小,当结果小于等于0时跳转
0xa5 if_acmpeq 比较栈顶两引用型数值,当结果相等时跳转
0xa6 if_acmpne 比较栈顶两引用型数值,当结果不相等时跳转
0xa7 goto 无条件跳转
0xa8 jsr 跳转至指定16位offset位置,并将jsr下一条指令地址压入栈顶
0xa9 ret 返回至本地变量指令的index的指令位置(一般与jsr或jsr_w联合使用)
0xaa tableswitch 用于switch条件跳转,case值连续(可变长度指令)
0xab lookupswitch 用于switch条件跳转,case值不连续(可变长度指令)
0xac ireturn 从当前方法返回int
0xad lreturn 从当前方法返回long
0xae freturn 从当前方法返回float
0xaf dreturn 从当前方法返回double
0xb0 areturn 从当前方法返回对象引用
0xb1 return 从当前方法返回void
0xc6 ifnull 为null时跳转
0xc7 ifnonnull 不为null时跳转
0xc8 goto_w 无条件跳转(宽索引)
0xc9 jsr_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指令根据键值在跳转表中寻找配对的分支并跳转,具体的格式如下图所示。

 【JVM源码解析】模板解释器解释执行Java字节码指令(下)_第3张图片

这是一条变长指令并且要求所有的操作数都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指令根据键值在跳转表中寻找配对的分支并跳转,具体的格式如下图所示。

【JVM源码解析】模板解释器解释执行Java字节码指令(下)_第4张图片

这是一条变长指令并且要求所有的操作数都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字节码指令(下)_第5张图片

你可能感兴趣的:(javajvm字节码)