揭秘JVM创世过程之两种语言首席外交官JavaCalls

前言

本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容可能存在疏漏,恳请读者不吝指正。

前情回顾

揭秘JVM创世过程之世界法则切换-从C++法则到Java法则中,提到主线程诞生后,JVM从“C++ 法则”切换到“Java法则”,也就是执行第一个字节码,由一个核心工具类JavaCalls完成。下面就来介绍一下JavaCalls机制。

JavaCalls 机制

在java世界可以将JVM看作一个拥有两种语言的领事馆:一边说 C++(系统语),另一边说 Java(字节码语)。那么 JavaCalls 就是那个身着正装、手里拿着翻译机的首席外交官
当 JVM 需要从 C++ 内部逻辑(如启动、反射、类初始化)去执行一段 Java 代码时,它必须通过 JavaCalls


1. 为什么不能直接调用?

在 C++ 里,调用函数遵循的是操作系统的 ABI(应用二进制接口),比如参数放进 RDI, RSI 寄存器。 而在 Java 世界里,方法调用遵循的是 JVM 规范:参数可能压在 Java 栈帧里,或者放进 JVM 自定义的寄存器中。

JavaCalls 的核心任务就是:

  1. 转换参数:把 C++ 的变量包装成 Java 能认出的样子。
  2. 切换栈帧:从 C++ 线程栈切到 Java 栈。
  3. 状态保护:处理安全点(Safepoint)检查,确保 GC 此时不会捣乱。

2. JavaCalls 的三驾马车

在源码中,你会看到这三个关键类:

  • JavaValue:用来接收 Java 方法执行后的返回值。
  • JavaCallArguments:一个参数“打包桶”,把 C++ 传递的 intoop(对象指针)等按顺序装好。
  • JavaCalls:执行引擎,包含 call_staticcall_virtualcall_special 等方法。

3. 源码深度剖析

JavaCalls 的主要实现在 src/share/vm/runtime/javaCalls.cpp。我们来看最核心的 JavaCalls::call 函数逻辑:

核心代码段 1:参数处理与准备

下面的代码是从call()方法执行过程角度给出的,这些代码是从相关代码合并到一起的。

void JavaCalls::call(JavaValue* result, methodHandle method, JavaCallArguments* args, TRAPS) {
  // 1. 检查方法是否为空,是否已经链接(link)
  if (method.is_null()) return;

  // 2. 获取目标方法的入口点(可能是解释器入口,也可能是 JIT 编译后的入口)
  address entry_point = method->from_interpreted_entry();

// TODO: 待补充真正执行到代码
  // 3. 线程状态切换:从 _thread_in_vm 切换到 _thread_in_Java
  // 这是为了让 GC 知道,这个线程现在跑的是 Java 代码,不能随便移动它引用的对象
  Thread* thread = THREAD;
  ThreadStateTransition::transition(thread, _thread_in_vm, _thread_in_Java);
}

以下代码是原始代码(删除部分不相关部分)
hotspot\src\share\vm\runtime\javaCalls.cppcall_static()方法涉及的源码


void JavaCalls::call_static(JavaValue* result, KlassHandle klass, Symbol* name, Symbol* signature, JavaCallArguments* args, TRAPS) {
  CallInfo callinfo;
  LinkResolver::resolve_static_call(callinfo, klass, name, signature, KlassHandle(), false, true, CHECK);
  methodHandle method = callinfo.selected_method();
  // 省略部分代码

  // Invoke the method
  JavaCalls::call(result, method, args, CHECK);
}

void JavaCalls::call(JavaValue* result, methodHandle method, JavaCallArguments* args, TRAPS) {
  // 省略部分代码

  // Need to wrap each and everytime, since there might be native code down the
  // stack that has installed its own exception handlers
  os::os_exception_wrapper(call_helper, result, &method, args, THREAD);
}

hotspot\src\share\vm\runtime\javaCalls.cppcall_helper()方法源码


void JavaCalls::call_helper(JavaValue* result, methodHandle* m, JavaCallArguments* args, TRAPS) {
  // 省略部分代码

  methodHandle method = *m;
  JavaThread* thread = (JavaThread*)THREAD;
  // 省略部分代码

  // Verify the arguments

  if (CheckJNICalls)  {
    args->verify(method, result->get_type(), thread);
  }
  else debug_only(args->verify(method, result->get_type(), thread));

  // Ignore call if method is empty
  if (method->is_empty_method()) {
    assert(result->get_type() == T_VOID, "an empty method must return a void value");
    return;
  }

  // Since the call stub sets up like the interpreter we call the from_interpreted_entry
  // so we can go compiled via a i2c. Otherwise initial entry method will always
  // run interpreted.
  address entry_point = method->from_interpreted_entry();
  // 省略部分代码

  // Figure out if the result value is an oop or not (Note: This is a different value
  // than result_type. result_type will be T_INT of oops. (it is about size)
  BasicType result_type = runtime_type_from(result);
  bool oop_result_flag = (result->get_type() == T_OBJECT || result->get_type() == T_ARRAY);

  // NOTE: if we move the computation of the result_val_address inside
  // the call to call_stub, the optimizer produces wrong code.
  intptr_t* result_val_address = (intptr_t*)(result->get_value_addr());

  // Find receiver
  Handle receiver = (!method->is_static()) ? args->receiver() : Handle();

  // 省略部分代码

  // do call
  { JavaCallWrapper link(method, receiver, result, CHECK);
    { HandleMark hm(thread);  // HandleMark used by HandleMarkCleaner

      StubRoutines::call_stub()(
        (address)&link,
        // (intptr_t*)&(result->_value), // see NOTE above (compiler problem)
        result_val_address,          // see NOTE above (compiler problem)
        result_type,
        method(),
        entry_point,
        args->parameters(),
        args->size_of_parameters(),
        CHECK
      );

      result = link.result();  // circumvent MS C++ 5.0 compiler bug (result is clobbered across call)
      // Preserve oop return value across possible gc points
      if (oop_result_flag) {
        thread->set_vm_result((oop) result->get_jobject());
      }
    }
  } // Exit JavaCallWrapper (can block - potential return oop must be preserved)


  // Restore possible oop return
  if (oop_result_flag) {
    result->set_jobject((jobject)thread->vm_result());
    thread->set_vm_result(NULL);
  }
}

核心代码段 2:通过 Call Stub 跳入 Java

这是最精彩的地方。JavaCalls 并不直接 jmp 到 Java 代码,而是通过一个**“跳板” (Call Stub)**。

  // 调用底层的 StubRoutines
  // 这里使用了函数指针调用,实际上执行的是一段预先生成的汇编代码
  StubRoutines::call_stub()(
    (address)&link,        // 链接器信息
    result_address,        // 返回值存放处
    result_type,           // 返回值类型
    method(),              // Method 对象指针
    entry_point,           // Java 入口地址
    args->parameters(),    // 参数列表
    args->size_of_parameters(), // 参数个数
    CHECK
  );

4. 那个神秘的 “Call Stub” 是什么?

StubRoutines::call_stub() 返回的不是 C++ 函数,而是 JVM 在启动时动态生成的一段汇编代码

这段汇编代码会做以下几件事:

  1. 清空/设置寄存器:按照 Java 虚拟机的规范对齐 CPU 寄存器。
  2. 压栈:把 JavaCallArguments 里的参数一个一个压入新的 Java 栈帧。
  3. 保存 C++ 现场:把当前的 C++ RSP(栈指针)和 RBP(基址指针)保存到某个特殊位置,以便回来时恢复。
  4. 真正起飞:执行 call entry_point

5. 执行流程全景图

当我们调用 System.initializeSystemClass() 时,流程如下:

  1. C++ 环境:在 create_vm 中意识到需要初始化 System 类。
  2. JavaCalls 介入:找到 initializeSystemClass 方法的指针,把 result 设为 void
  3. 状态切换:当前线程标记为 _thread_in_Java
  4. 汇编跳板:执行 Call Stub,把参数塞进栈,把 CPU 控制权交给解释器。
  5. Java 领空:解释器开始一行行运行 Java 字节码。
  6. 着陆返回:Java 执行完 return,汇编代码清理 Java 栈,恢复 C++ 寄存器,线程状态切回 _thread_in_vm

6. 为什么我们要关心它?

  • 性能瓶颈:频繁地在 C++ 和 Java 之间来回切换(跨越 JavaCalls)是有开销的。这就是为什么 JNI 调用如果太频繁会变慢的原因之一。
  • 调试意义:如果你在看 core dump(崩溃堆栈),看到堆栈里出现了 StubRoutinesJavaCalls::call,你就知道当前程序正处于跨语言调用的临界点

总结

JavaCalls 就是 JVM 的**“时空门”**。它通过:

  1. C++ 层的封装(处理逻辑)
  2. 汇编层的 Stub(处理物理栈转换)
  3. 线程状态的切换(处理垃圾回收安全)

完美地解决了底层系统代码与高层逻辑代码之间的隔离与通信。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值