欢迎光临
我们一直在努力

揭秘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 的核心任务就是:

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

  • 2. JavaCalls 的三驾马车

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

    • JavaValue:用来接收 Java 方法执行后的返回值。
    • JavaCallArguments:一个参数“打包桶”,把 C++ 传递的 int、oop(对象指针)等按顺序装好。
    • JavaCalls:执行引擎,包含 call_static、call_virtual、call_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.cpp中call_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.cpp中call_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 在启动时动态生成的一段汇编代码。

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

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

  • 5. 执行流程全景图

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

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

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

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

    总结

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

  • C++ 层的封装(处理逻辑)
  • 汇编层的 Stub(处理物理栈转换)
  • 线程状态的切换(处理垃圾回收安全)
  • 完美地解决了底层系统代码与高层逻辑代码之间的隔离与通信。

    赞(0)
    未经允许不得转载:171主机测评 » 揭秘JVM创世过程之两种语言首席外交官JavaCalls
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址