我们在之前介绍过return字节码指令的执行逻辑,这个字节码指令只会执行释放锁和退出当前栈帧的操作,但是当控制权转移给调用者时,还需要恢复调用者的栈帧状态,如让%r13指向bcp、%r14指向局部变量表等,另外还需要弹出压入的实参、跳转到调用者的下一个字节码指令继续执行,而这一切操作都是由Interpreter::_return_entry例程负责的。这个例程在之前介绍invokevirtual和invokeinterface等字节码指令时介绍过,当使用这些字节码指令调用方法时,会根据方法的返回类型压入Interpreter::_return_entry一维数组中保存的对应例程地址,这样return字节码指令执行完成后就会执行这段例程。
在invokevirtual和invokeinterface等字节码指令中通过调用如下函数获取对应的例程入口:
address* TemplateInterpreter::invoke_return_entry_table_for(Bytecodes::Code code) { switch (code) { case Bytecodes::_invokestatic: case Bytecodes::_invokespecial: case Bytecodes::_invokevirtual: case Bytecodes::_invokehandle: return Interpreter::invoke_return_entry_table(); case Bytecodes::_invokeinterface: return Interpreter::invokeinterface_return_entry_table(); default: fatal(err_msg("invalid bytecode: %s", Bytecodes::name(code))); return NULL; } }
可以看到invokeinterface字节码从Interpreter::_invokeinterface_return_entry数组中获取对应的例程,而其它的从Interpreter::_invoke_return_entry一维数组中获取。如下:
address TemplateInterpreter::_invoke_return_entry[TemplateInterpreter::number_of_return_addrs]; address TemplateInterpreter::_invokeinterface_return_entry[TemplateInterpreter::number_of_return_addrs]; address TemplateInterpreter::_invokedynamic_return_entry[TemplateInterpreter::number_of_return_addrs];
当返回一维数组后,会根据方法返回类型进一步确定例程入口地址。下面我们就看一下这些例程的生成过程。
TemplateInterpreterGenerator::generate_all()函数中会生成Interpreter::_return_entry入口,如下:
{ CodeletMark cm(_masm, "invoke return entry points"); const TosState states[] = {itos, itos, itos, itos, ltos, ftos, dtos, atos, vtos}; const int invoke_length = Bytecodes::length_for(Bytecodes::_invokestatic); // invoke_length=3 const int invokeinterface_length = Bytecodes::length_for(Bytecodes::_invokeinterface); // invokeinterface=5 for (int i = 0; i < Interpreter::number_of_return_addrs; i++) { // number_of_return_addrs = 9 TosState state = states[i]; // TosState是枚举类型 Interpreter::_invoke_return_entry[i] = generate_return_entry_for(state, invoke_length, sizeof(u2)); Interpreter::_invokeinterface_return_entry[i] = generate_return_entry_for(state, invokeinterface_length, sizeof(u2)); } }
除invokedynamic字节码指令外,其它的方法调用指令在解释执行完成后都需要调用由generate_return_entry_for()函数生成的例程,生成例程的generate_return_entry_for()函数实现如下:
address TemplateInterpreterGenerator::generate_return_entry_for(TosState state, int step, size_t index_size) { // Restore stack bottom in case万一 i2c adjusted stack __ movptr(rsp, Address(rbp, frame::interpreter_frame_last_sp_offset * wordSize)); // interpreter_frame_last_sp_offset=-2 // and NULL it as marker that esp is now tos until next java call __ movptr(Address(rbp, frame::interpreter_frame_last_sp_offset * wordSize), (int32_t)NULL_WORD); __ restore_bcp(); __ restore_locals(); // ... const Register cache = rbx; const Register index = rcx; __ get_cache_and_index_at_bcp(cache, index, 1, index_size); const Register flags = cache; __ movl(flags, Address(cache, index, Address::times_ptr, ConstantPoolCache::base_offset() + ConstantPoolCacheEntry::flags_offset())); __ andl(flags, ConstantPoolCacheEntry::parameter_size_mask); __ lea(rsp, Address(rsp, flags, Interpreter::stackElementScale()) ); // 栈元素标量为8 __ dispatch_next(state, step); return entry; }
根据state的不同(方法的返回类型的不同),会在选择执行调用者方法的下一个字节码指令时,决定要从字节码指令的哪个入口处开始执行。我们看一下,当传递的state为itos(也就是当方法的返回类型为int时)时生成的汇编代码如下:
// 将-0x10(%rbp)存储到%rsp后,置空-0x10(%rbp) 0x00007fffe1006ce0: mov -0x10(%rbp),%rsp // 更改rsp 0x00007fffe1006ce4: movq $0x0,-0x10(%rbp) // 更改栈中特定位置的值 // 恢复bcp和locals,使%r14指向本地变量表,%r13指向bcp 0x00007fffe1006cec: mov -0x38(%rbp),%r13 0x00007fffe1006cf0: mov -0x30(%rbp),%r14 // 获取ConstantPoolCacheEntry的索引并加载到%ecx 0x00007fffe1006cf4: movzwl 0x1(%r13),%ecx // 获取栈中-0x28(%rbp)的ConstantPoolCache并加载到%ecx 0x00007fffe1006cf9: mov -0x28(%rbp),%rbx // shl是逻辑左移,获取字偏移 0x00007fffe1006cfd: shl $0x2,%ecx // 获取ConstantPoolCacheEntry中的_flags属性值 0x00007fffe1006d00: mov 0x28(%rbx,%rcx,8),%ebx // 获取_flags中的低8位中保存的参数大小 0x00007fffe1006d04: and $0xff,%ebx // lea指令将地址加载到内存寄存器中,也就是恢复调用方法之前栈的样子 0x00007fffe1006d0a: lea (%rsp,%rbx,8),%rsp // 跳转到下一指令执行 0x00007fffe1006d0e: movzbl 0x3(%r13),%ebx 0x00007fffe1006d13: add $0x3,%r13 0x00007fffe1006d17: movabs $0x7ffff73b7ca0,%r10 0x00007fffe1006d21: jmpq *(%r10,%rbx,8)
如上汇编代码的逻辑非常简单,这里不再过多介绍。
公众号 深入剖析Java虚拟机HotSpot 已经更新虚拟机源代码剖析相关文章到60+,欢迎关注,如果有任何问题,可加作者微信mazhimazh,拉你入虚拟机群交流