news 2026/5/20 23:34:13

揭秘JVM创世过程之Call Stub进入Java世界的门票

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
揭秘JVM创世过程之Call Stub进入Java世界的门票

前言

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

前情回顾

在揭秘JVM创世过程之两种语言首席外交官JavaCalls,一文中将JVM看作Java世界中一个拥有两种语言的领事馆:一边说C++(系统语),另一边说Java(字节码语)。那么JavaCalls就是那个身着正装、手里拿着翻译机的首席外交官。当 JVM 需要从 C++ 内部逻辑(如启动、反射、类初始化)去执行一段 Java 代码时,它必须通过JavaCalls。而JavaCalls最精彩的地方并不直接 jmp 到 Java 代码,而是通过一个**“跳板” (Call Stub)**。

JVM 在启动时是如何生成这段“跳板(Call Stub)”汇编代码

进入Java世界的门票“跳板”代码(Call Stub)的生成过程,是 JVM 启动时最硬核的底层操作之一。它不是由编译器提前编译好的,而是 JVM 在运行初期,直接在内存里**“现场手写”**出来的机器码。

这个过程主要由StubGeneratorMacroAssembler这两个组件协作完成。


1. 核心流程:从 C++ 到机器码

Threads::create_vm执行过程中,会调用init_globals(),进而触发stubRoutines_init1()。这时,JVM 会开启“机器码印刷机”。

  • 整个执行过程相关核心代码如下:
  1. hotspot\src\share\vm\runtime\thread.cppThreads::create_vm()核心代码如下:
jintThreads::create_vm(JavaVMInitArgs*args,bool*canTryAgain){// 省略部分代码// Attach the main thread to this os threadJavaThread*main_thread=newJavaThread();main_thread->set_thread_state(_thread_in_vm);main_thread->record_stack_base_and_size();main_thread->initialize_thread_local_storage();main_thread->set_active_handles(JNIHandleBlock::allocate_block());// 省略部分代码// Initialize global modules// 在此方法中会执行stubRoutines_init1(),完成“跳板”代码(Call Stub)jint status=init_globals();if(status!=JNI_OK){deletemain_thread;*canTryAgain=false;// don't let caller call JNI_CreateJavaVM againreturnstatus;}// 省略部分代码{// The VM creates & returns objects of this class. Make sure it's initialized.initialize_class(vmSymbols::java_lang_Class(),CHECK_0);// The VM preresolves methods to these classes. Make sure that they get initializedinitialize_class(vmSymbols::java_lang_reflect_Method(),CHECK_0);initialize_class(vmSymbols::java_lang_ref_Finalizer(),CHECK_0);call_initializeSystemClass(CHECK_0);// 省略部分代码}// 省略部分代码}
  1. init_globals()方法核心代码
    hotspot\src\share\vm\runtime\init.cppinit_globals()中调用stubRoutines_init1()stubRoutines_init2()完成stubRoutines初始化。
jintinit_globals(){// 省略部分代码bytecodes_init();classLoader_init();codeCache_init();VM_Version_init();os_init_globals();stubRoutines_init1();jint status=universe_init();// dependent on codeCache_init and// stubRoutines_init1 and metaspace_init.if(status!=JNI_OK)returnstatus;javaClasses_init();// must happen after vtable initializationstubRoutines_init2();// note: StubRoutines need 2-phase init// 省略部分代码returnJNI_OK;}
  1. stubRoutines_init1()方法核心代码

hotspot\src\share\vm\runtime\stubRoutines.cpp

voidstubRoutines_init1(){StubRoutines::initialize1();}voidStubRoutines::initialize1(){if(_code1==NULL){ResourceMark rm;TraceTimetimer("StubRoutines generation 1",TraceStartupTime);_code1=BufferBlob::create("StubRoutines (1)",code_size1);if(_code1==NULL){vm_exit_out_of_memory(code_size1,OOM_MALLOC_ERROR,"CodeCache: no room for StubRoutines (1)");}CodeBufferbuffer(_code1);StubGenerator_generate(&buffer,false);}}
Step 1: 申请可执行内存 (Code Buffer)

JVM 会在内存中开辟一块特殊的区域(属于 CodeCache 的一部分),并将其权限设置为可读、可写、可执行(RWX)。这块内存就是用来存放生成的汇编指令的“纸”。

hotspot\src\share\vm\runtime\stubRoutines.cpp在方法initialize1()中通过BufferBlob::create()申请一块内存,代码如下:

voidStubRoutines::initialize1(){if(_code1==NULL){ResourceMark rm;TraceTimetimer("StubRoutines generation 1",TraceStartupTime);_code1=BufferBlob::create("StubRoutines (1)",code_size1);if(_code1==NULL){vm_exit_out_of_memory(code_size1,OOM_MALLOC_ERROR,"CodeCache: no room for StubRoutines (1)");}CodeBufferbuffer(_code1);StubGenerator_generate(&buffer,false);}}
Step 2: 启动 MacroAssembler (宏汇编器)

JVM 使用一个名为MacroAssembler的 C++ 类。这个类非常神奇,它把每一条 CPU 指令(如push,mov,call)都封装成了一个 C++ 函数。

  • 当你调用masm->push(rax)时,它并不是在执行 push,而是在往刚才申请的内存缓冲区里写入0x50(x86-64 架构下push rax的机器码)。

2. 生成 Call Stub 的具体代码逻辑

在 OpenJDK 源码中(以 x86_64 为例),生成这段代码的逻辑位于src/hotspot/cpu/x86/stubGenerator_x86_64.cppgenerate_call_stub方法中。

我们可以通过 C++ 源码窥见它“手写”汇编的过程:

addressgenerate_call_stub(address&return_address){// ... 准备缓冲区 ...StubCodeMarkmark(this,"StubRoutines","call_stub");address start=__pc();// 记录起始地址// 1. 保存调用者(C++)的现场__enter();// push rbp; mov rbp, rsp__push(r15);// 保存关键寄存器__push(r14);__push(r13);__push(r12);// 2. 将 C++ 传进来的参数移动到指定的寄存器// 比如从 c_rarg1 (rsi) 拿到 Java 方法的入口地址__movptr(rbx,method);// 把 Method* 放入 rbx// 3. 构建 Java 栈帧// 这里会根据参数个数动态调整 rsp 的位置,为 Java 参数腾地方// 4. 关键一跃:跳入 Java 世界__call(entry_point);// 这里的 entry_point 就是解释器的入口// 5. 凯旋归来:清理 Java 栈并恢复 C++ 寄存器__pop(r12);__pop(r13);__pop(r14);__pop(r15);__leave();__ret(0);// 返回到 JavaCalls::callreturnstart;// 返回这段代码在内存中的首地址}

3. 如何变成函数指针?

生成完这段机器码后,JVM 会做一个非常关键的动作:

  1. 记录地址:将start(这段代码的首地址)存入全局变量StubRoutines::_call_stub_entry

  2. 强制转换:在头文件中,这个地址被定义为一个复杂的函数指针类型。

  3. 调用:当JavaCalls需要执行 Java 代码时,它直接通过这个指针调用:

(*(CallStub)cast_to_fqn(_call_stub_entry))(...)

这时,CPU 就会从当前的 C++ 指令流,直接跳转到那块刚才生成的内存区域,开始执行那几条pushmov


4. 为什么要“现场手写”而不是写在.s汇编文件里?

你可能会问:为什么不直接写个.s汇编文件编译进来?

  • 动态性:JVM 需要根据当前的硬件特性(比如是否支持 AVX-512 指令集、是否开启了某些安全补丁)来动态决定生成什么样的指令。
  • 极致优化:现场生成可以根据当前的偏移量计算出最短的跳转指令(Short Jump),减少代码体积。
  • 统一抽象:通过 C++ 宏汇编器,JVM 可以用一套逻辑在不同的系统(Linux/Windows/macOS)上生成对应的机器码,而不需要维护无数个版本的.s文件。

总结

JVM 生成跳板代码的过程,就像是一个在施工现场直接烧制异形砖块的建筑师

  1. MacroAssembler是模具。
  2. 内存缓冲区是原材料。
  3. StubGenerator是施工图纸。

这种“运行时生成代码”的技术(JIT 思想的萌芽),确保了 Java 能够跨越 C++ 和字节码的鸿沟,同时保持顶级的执行效率。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/1 22:44:34

【图形学】CS:GO 的 “Uber 着色器” 是啥?

【图形学】CS:GO 的 “Uber 着色器” 是啥? 虽然我们进入了起源 2 的 CS2 时代,但 CS:GO 仍然具有很大的惯性,我们对 CS:GO 的部分疑问还没有解除。那就是画质菜单选项的 “启用 Uber 着色器” 是啥意思?包括很多起源开发者也认为…

作者头像 李华
网站建设 2026/4/5 21:21:34

Splashtop亮相知行社第453期沙龙,筑牢AI智能体时代的远程运维底座

以安全高效的远程连接能力,赋能IT企业转型与AI时代服务升级概览2026年3月27日,北京知行社第453期学习沙龙圆满举办。本期沙龙以“智能体时代下IT企业的转型之路”为核心议题,汇聚四十余家 IT 行业企业负责人,围绕 AI 智能体从“对…

作者头像 李华
网站建设 2026/4/7 13:18:43

从ERP到APO:手把手拆解CIF接口如何“搬运”你的生产主数据

从ERP到APO:CIF接口如何实现生产主数据的精准同步 当SAP APO系统中的生产数据与ERP源头出现偏差时,技术团队往往会陷入数据迷宫。这种不一致性可能引发生产排程失效、物料需求计算错误等一系列连锁反应。本文将带您深入CIF接口的传输机制,揭示…

作者头像 李华
网站建设 2026/4/1 22:43:27

多场景适配:ClearerVoice-Studio支持16K/48K采样率,会议直播都适用

多场景适配:ClearerVoice-Studio支持16K/48K采样率,会议直播都适用 1. 为什么音频采样率如此重要? 在语音处理领域,采样率选择直接影响最终效果。就像相机像素决定照片清晰度一样,音频采样率决定了声音的"分辨率…

作者头像 李华