JVM方法调用——java之间

Java方法之间

解释方法到解释方法

进入

解释方法到解释方法是最为简单的一种情况,最常见的调用是invokevirtual。有关的代码在TemplateTable::invokevirtual中:

void TemplateTable::invokevirtual(int byte_no) {
  transition(vtos, vtos);
  assert(byte_no == f2_byte, "use this argument");
  // 让寄存器保存对应值,同时,保存rbcp以及确定返回地址。
  prepare_invoke(byte_no,
                 rbx,    // method or vtable index
                 noreg,  // unused itable index
                 rcx, rdx); // recv, flags

  // rbx: index(其实是method)
  // rcx: receiver
  // rdx: flags

  invokevirtual_helper(rbx, rcx, rdx);
}

有关的逻辑并不多,所以可以看完它们,首先来看看prepare_invoke

void TemplateTable::prepare_invoke(int byte_no,
                                   Register method,  // linked method (or i-klass)
                                   Register index,   // itable index, MethodType, etc.
                                   Register recv,    // if caller wants to see it
                                   Register flags    // if caller wants to test it
                                   ) {
  // determine flags
  const Bytecodes::Code code = bytecode();
  const bool is_invokeinterface  = code == Bytecodes::_invokeinterface;
  const bool is_invokedynamic    = code == Bytecodes::_invokedynamic;
  const bool is_invokehandle     = code == Bytecodes::_invokehandle;
  const bool is_invokevirtual    = code == Bytecodes::_invokevirtual;
  const bool is_invokespecial    = code == Bytecodes::_invokespecial;
  const bool load_receiver       = (recv  != noreg); // 非静态的调用。
  const bool save_flags          = (flags != noreg);
  assert(load_receiver == (code != Bytecodes::_invokestatic && code != Bytecodes::_invokedynamic), "");
  assert(save_flags    == (is_invokeinterface || is_invokevirtual), "need flags for vfinal");
  assert(flags == noreg || flags == rdx, "");
  assert(recv  == noreg || recv  == rcx, "");

  // setup registers & access constant pool cache
  if (recv  == noreg)  recv  = rcx;
  if (flags == noreg)  flags = rdx;
  assert_different_registers(method, index, recv, flags);

  // save 'interpreter return address'
  __ save_bcp(); // 将当前rbcp保存在当前栈的rbcp中(还没有到新的栈)。

  // 将方法指针放入method寄存器,将flags放入flags寄存器。
  load_invoke_cp_cache_entry(byte_no, method, index, flags, is_invokevirtual, false, is_invokedynamic);

  // maybe push appendix to arguments (just before return address)
  if (is_invokedynamic || is_invokehandle) {
    Label L_no_push;
    __ testl(flags, (1 << ConstantPoolCacheEntry::has_appendix_shift));
    __ jcc(Assembler::zero, L_no_push);
    // Push the appendix as a trailing parameter.
    // This must be done before we get the receiver,
    // since the parameter_size includes it.
    __ push(rbx);
    __ mov(rbx, index);
    assert(ConstantPoolCacheEntry::_indy_resolved_references_appendix_offset == 0, "appendix expected at index+0");
    __ load_resolved_reference_at_index(index, rbx);
    __ pop(rbx);
    __ push(index);  // push appendix (MethodType, CallSite, etc.)
    __ bind(L_no_push);
  }

  // load receiver if needed (after appendix is pushed so parameter size is correct)
  // Note: no return address pushed yet
  if (load_receiver) {
    __ movl(recv, flags);
    __ andl(recv, ConstantPoolCacheEntry::parameter_size_mask);
    const int no_return_pc_pushed_yet = -1;  // argument slot correction before we push return address
    const int receiver_is_at_end      = -1;  // back off one slot to get receiver
    Address recv_addr = __ argument_address(recv, no_return_pc_pushed_yet + receiver_is_at_end);
    __ movptr(recv, recv_addr); // 通过对rsp的偏移获取receiver。
    __ verify_oop(recv);
  }

  if (save_flags) {
    // 原有的rbcp已经保存了。
    __ movl(rbcp, flags);
  }

  // compute return type
  __ shrl(flags, ConstantPoolCacheEntry::tos_state_shift); // 将其他位全部丢弃。
  // Make sure we don't need to mask flags after the above shift
  ConstantPoolCacheEntry::verify_tos_state_shift();
  // load return address
  {
    const address table_addr = (address) Interpreter::invoke_return_entry_table_for(code);
    ExternalAddress table(table_addr); // 大致明白了,ExternalAddress就是虚拟机程序静态段的数据。
    LP64_ONLY(__ lea(rscratch1, table));
    LP64_ONLY(__ movptr(flags, Address(rscratch1, flags, Address::times_ptr))); // 根据返回类型获得返回例程。
    NOT_LP64(__ movptr(flags, ArrayAddress(table, Address(noreg, flags, Address::times_ptr))));
  }
  // 让栈布局和从call_stub中进入是一样的。
  // 在此处放入了返回地址,返回到一个invoke_return_entry中。
  // push return address
  __ push(flags);

  // Restore flags value from the constant pool cache, and restore rsi
  // for later null checks.  r13 is the bytecode pointer
  if (save_flags) {
    // 恢复上面的保存操作。
    __ movl(flags, rbcp);
    __ restore_bcp();
  }

  // 结束上面所有的指令之后栈的状态:
  // arg-1, ..., arg-n, ret-addr
  // -> 增长方向。
}

27:将当前的bcp保存到了栈中对应位置(上图bcp),在返回的时候会使用,需要注意的是保存的不是下一条指令的bcp,而是当前指令的bcp。

30:如果知道ConstantPoolCache并不难理解,关于这部分的内容可以看其他内容。

33~47:直接跳过,只关注invokevirtual

51~59:获取receiver,跳进argument_address看看就知道发生了什么。

71~77:这个地方很重要,此时flags中放的是返回类型,然后根据返回类型从invoke_return_entry_table中获取对应的返回例程,将这个返回例程放入flags寄存器。

那么开始下一段代码,不过先记住当前栈和寄存器的状态:

statckframe-after-prepare_invoke.drawio

  // rbx: index(vtable的index)
  // rcx: receiver
  // rdx: flags
void TemplateTable::invokevirtual_helper(Register index,
                                         Register recv,
                                         Register flags) {
  // Uses temporary registers rax, rdx
  assert_different_registers(index, recv, rax, rdx); // index和recv需要保存,flags不用。
  assert(index == rbx, "");
  assert(recv  == rcx, "");

  // Test for an invoke of a final method
  Label notFinal;
  __ movl(rax, flags);
  __ andl(rax, (1 << ConstantPoolCacheEntry::is_vfinal_shift));
  __ jcc(Assembler::zero, notFinal);

  const Register method = index;  // method must be rbx
  assert(method == rbx,
         "Method* must be rbx for interpreter calling convention");

  // do the call - the index is actually the method to call
  // that is, f2 is a vtable index if !is_vfinal, else f2 is a Method*

  // It's final, need a null check here!
  __ null_check(recv);

  // profile this call
  __ profile_final_call(rax);
  __ profile_arguments_type(rax, method, rbcp, true);

  __ jump_from_interpreted(method, rax);

  __ bind(notFinal);

  // get receiver klass
  __ null_check(recv, oopDesc::klass_offset_in_bytes());
  __ load_klass(rax, recv);

  // profile this call
  __ profile_virtual_call(rax, rlocals, rdx);
  // get target Method* & entry point
  __ lookup_virtual_method(rax, index, method); // 将method指针放在method寄存器。
  __ profile_called_method(method, rdx, rbcp);

  __ profile_arguments_type(rdx, method, rbcp, true);
  __ jump_from_interpreted(method, rdx);
}

忽略掉profile开头的一些函数,其他的部分都非常直观,就是从vtable中获取方法的指针,最终会跳入jump_from_interpreted生成的代码中:

// Jump to from_interpreted entry of a call unless single stepping is possible
// in this thread in which case we must call the i2i entry
void InterpreterMacroAssembler::jump_from_interpreted(Register method, Register temp) {
  prepare_to_jump_from_interpreted();

  if (JvmtiExport::can_post_interpreter_events()) {
    Label run_compiled_code;
    // JVMTI events, such as single-stepping, are implemented partly by avoiding running
    // compiled code in threads for which the event is enabled.  Check here for
    // interp_only_mode if these events CAN be enabled.
    // interp_only is an int, on little endian it is sufficient to test the byte only
    // Is a cmpl faster?
    LP64_ONLY(temp = r15_thread;)
    NOT_LP64(get_thread(temp);)
    cmpb(Address(temp, JavaThread::interp_only_mode_offset()), 0); // 获取参数。
    jccb(Assembler::zero, run_compiled_code);
    jmp(Address(method, Method::interpreter_entry_offset())); // 如果只解释执行就只会获取_i2i_entry。
    bind(run_compiled_code);
  }

  jmp(Address(method, Method::from_interpreted_offset())); // 获取_from_interpreted_entry,可能是个i2c_adapter。
  // 解释模式下跳转到generate_normal_entry生成的代码。
}

和Jvmti有关的部分我也看不懂,不过不影响大体的逻辑,就是从method中获得了入口,然后跳转到入口(由TemplateInterpreterGenerator::generate_normal_entry生成的),不过还是要注意一下prepare_to_jump_from_interpreted这个函数,里面的逻辑很简单就是将sender_sp放在了_bcp_register寄存器中,同时将当前栈帧中(caller)的last_sp进行更新:

void InterpreterMacroAssembler::prepare_to_jump_from_interpreted() {
  // 之前找不到generate_normal_entry中的sender rsp如何被设置的,这里发现了。
  // set sender sp
  lea(_bcp_register, Address(rsp, wordSize));
  // record last_sp
  movptr(Address(rbp, frame::interpreter_frame_last_sp_offset * wordSize), _bcp_register);
}

执行完毕后caller栈的情况是这样的:

stackframe-after-prepare_to_jump_from_interpreted.drawio

解释执行的方法的通用入口由TemplateInterpreterGenerator::generate_normal_entry生成,为了更加简洁一些我删掉了一些代码,被删除的代码和synchronized和profile有关:

// 通用的解释器方法入口,解释器进行调用解释器函数时会使用这个作为入口。
// 所以它会构造一个java方法栈。
//
// Generic interpreted method entry to (asm) interpreter
//
address TemplateInterpreterGenerator::generate_normal_entry(bool synchronized) {
  // 执行下面生成代码时栈状态:
  // arg1, ..., argn, ret-addr
  // -> 增长方向。
    
  // ebx: Method*
  // rbcp: sender sp
  address entry_point = __ pc();

  const Address constMethod(rbx, Method::const_offset());
  const Address access_flags(rbx, Method::access_flags_offset());
  const Address size_of_parameters(rdx,
                                   ConstMethod::size_of_parameters_offset());
  const Address size_of_locals(rdx, ConstMethod::size_of_locals_offset());


  // get parameter size (always needed)
  __ movptr(rdx, constMethod);
  __ load_unsigned_short(rcx, size_of_parameters);

  // rbx: Method*
  // rcx: size of parameters
  // rbcp: sender_sp (could differ from sp+wordSize if we were called via c2i )

  __ load_unsigned_short(rdx, size_of_locals); // get size of locals in words
  __ subl(rdx, rcx); // rdx = no. of additional locals
  // 此时rdx中为非参数的locals。

  // get return address
  __ pop(rax); // 将ret-addr弹出。

  // rlocals在这个过程中不会使用,但是确实一个约定的寄存器,用来保存locals的开始位置(从arg1开始)。
  // compute beginning of parameters
  __ lea(rlocals, Address(rsp, rcx, Interpreter::stackElementScale(), -wordSize));

  // 创建非参数的locals。
  // rdx - # of additional locals
  // allocate space for locals
  // explicitly initialize locals
  {
    Label exit, loop;
    __ testl(rdx, rdx);
    __ jcc(Assembler::lessEqual, exit); // do nothing if rdx <= 0
    __ bind(loop);
    __ push((int) NULL_WORD); // initialize local variables
    __ decrementl(rdx); // until everything initialized
    __ jcc(Assembler::greater, loop);
    __ bind(exit);
  }

  // initialize fixed part of activation frame
  generate_fixed_frame(false);

    
  // step=0就是将bcp指向的字节码装载到rbx。
  __ dispatch_next(vtos);

  return entry_point;
}

31:计算需要多少额外的local slot,java的locals表由两个部分组成,一个部分是传入的参数的slot,另外一部分就是需要额外生成的。

35:将返回地址暂时弹出,因为locals是连续的,中间不应该有东西分隔。

39:设置了locals指针。

45~54:为额外的local slot开辟空间。

57:生成固定部分,固定部分入图1所示。

61:跳转到目标方法的字节码起始处,开始执行,在TemplateInterpreterGenerator::generate_fixed_frame重新设置了rbcp指针。

结合图1TemplateInterpreterGenerator::generate_fixed_frame的逻辑十分直观,这里就不说了。

返回

在返回之前,解释器会执行ireturn字节码,假设方法返回一个int类型的返回值。来看看生成ireturn模板代码的函数,删掉了一些和安全点有关的代码:

void TemplateTable::_return(TosState state) {
  transition(state, state);

  // Narrow result if state is itos but result type is smaller.
  // Need to narrow in the return bytecode rather than in generate_return_entry
  // since compiled code callers expect the result to already be narrowed.
  if (state == itos) {
    __ narrow(rax);
  }
  __ remove_activation(state, rbcp);
  // 回到callee栈了,rsp指向最后一个参数。
  __ jmp(rbcp);
}

这里的逻辑非常简单,主要的处理其实在InterpreterMacroAssembler::remove_activation里面,调用这个函数,将栈的状态恢复,将返回地址放入rbcp,返回值在(tos)rax,如果是32位则是eax/edx。

它是一个很长的函数,但是我们删掉monitor、Jvmti、溢出保留页这些逻辑之后整个函数变得特别简单:

// remove activation
void InterpreterMacroAssembler::remove_activation(
        TosState state,
        Register ret_addr,
        bool throw_monitor_exception,
        bool install_monitor_exception,
        bool notify_jvmdi) {
  // 最简单的情况下的执行路径非常简单,就是获取sender rsp,然后设置回去就行了。

  // rax, rdx: Might contain return value

  // remove activation
  // get sender sp
  movptr(rbx,
         Address(rbp, frame::interpreter_frame_sender_sp_offset * wordSize));

  leave();                           // remove frame anchor
  pop(ret_addr);                     // get return address
  mov(rsp, rbx);                     // set sp to sender sp
}

这个函数生成的代码执行结束之后栈的状态如下:

interpreter-stackframe-after-remove_activation.drawio

我们之前设置了返回点,还记得么?就在TemplateTable::prepare_invoke中,返回例程是在TemplateInterpreterGenerator::generate_return_entry_for中生成的:

// 调用返回例程,会自动获取下一条指令。
address TemplateInterpreterGenerator::generate_return_entry_for(TosState state, int step, size_t index_size) {
  address entry = __ pc();

  // Restore stack bottom in case i2c adjusted stack
  __ movptr(rsp, Address(rbp, frame::interpreter_frame_last_sp_offset * wordSize));
  // 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()));

  __ dispatch_next(state, step);

  return entry;
}

6:再次执行了一次恢复rsp。

8~12:将寄存器恢复到之前的状态。

19~22:出栈全部参数。

24:跳转到下一条指令。

至此,调用完成,如果有返回值,栈顶缓存(tos)保存了返回值。

编译方法到编译方法

解释方法到编译方法

从解释方法到编译方法以及从编译方法

到解释方法,事实上都会使用adapter,也就是适配器。

Method对象有3个入口:

  1. _i2i_entry(从解释器到解释器入口)
  2. _from_compiled_entry(从已编译代码进入此方法的入口)
  3. _from_interpreted_entry(从解释器代码进入此方法的入口)

第三个_from_interpreted_entry一开始和_i2i_entry指向同一段代码,但是在方法被编译之后就会指向

_i2i_entry是固定的,并不会随着

// get_adapter0

编译方法到解释方法

参考

posted @ 2022-09-21 22:49  aana  阅读(166)  评论(0编辑  收藏  举报