1. 从一道面试题聊起:JVM里那张“与生俱来”的地图
我当初入行Java时,第一次被问到“JVM启动过程中,最先准备好的代码是什么”这个问题,整个人是懵的。你背过类加载、背过双亲委派、背过对象创建流程,但很少有人告诉你,HotSpot在Java世界还没苏醒之前,自己先手写了一批“原生代码片段”,它们就是StubRoutines——我习惯叫它“Java世界的地图”。
为什么说它是地图?因为JVM本身是用C++写的,但它要解析字节码、调用系统原生方法、处理异常、做GC时的内存复制……这些“翻译”和“跳转”动作,不能临时抱佛脚去查表,必须在启动早期就把一套固定套路生成好、固化在内存里。StubRoutines就是这套固定套路的总称,它是HotSpot在启动阶段生成的一系列机器码例程。你写的第一行Java代码还没跑起来,这些例程已经躺在内存里等着被召唤了。
这类知识属于典型的“面试不会重点考,但考到就能拉开差距”的内容。如果你正在准备JVM相关的面试,或者想看明白hs_err日志里那些XX_code_stub的地址段,再或者对“解释器怎么跳进JIT编译后的代码”这种机制有好奇心,这篇文章就是写给你的。我会从StubRoutines的生成时机、内部结构、典型类型,一路讲到实际排查问题时会遇到的表现,尽量不堆术语,讲点真正能在实践中用上的东西。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. StubRoutines不是什么黑科技,它是HotSpot的“预制菜”
2.1 先理解一个前提:JVM自己也是程序
很多Java开发者容易把JVM当成一个黑盒,觉得它天生就能跑Java代码。但回过神想一想,JVM本身是一个用C++写成的进程,它启动时要先做自己的运行时初始化,比如分配内存、初始化全局数据结构、解析JVM参数,然后才具备“执行Java字节码”的能力。
问题来了:执行Java字节码需要什么?至少需要一套“从机器码到字节码”或“从字节码到机器码”的桥接逻辑。HotSpot的架构中,解释器(Interpreter)负责一条一条解析字节码,JIT编译器(C1/C2)负责把热点方法编译成本地机器码。但不管是解释器还是JIT产物,它们生成的代码都不是凭空出现的——解释器启动时要把自己的模板代码准备好,JIT编译器也要依赖一批生成好的辅助例程。
这批辅助例程,就是StubRoutines。它们的作用有点像餐厅后厨提前切好的菜:客人点单(Java方法调用)的时候,主厨不需要现场去洗菜切菜,直接拿预制菜下锅就行。JVM里那些高频的、固定的、平台相关的操作,提前生成好机器码,运行时直接跳过去执行,避免了每次都在C++层做分支判断或函数调用的开销。
2.2 StubRoutines和JIT编译结果不是一回事
这里有第一个容易混淆的点,必须拎清楚。很多人听到“JVM生成的机器码”,先想到的是JIT编译后的方法代码。但StubRoutines和JIT编译产物有本质区别:
- JIT编译产物是“跟着业务走”的,某个方法被判定为热点,编译成优化后的机器码,存进CodeCache,它服务于具体的Java方法。
- StubRoutines是“跟着JVM走”的,不管你的业务代码是什么,只要JVM启动,这批例程就一定会生成,它们服务于JVM自身的运行机制。
你可以把JIT产物理解成裁缝按客人身材做的定制西装,而StubRoutines是店面里挂着的成衣——无论谁来,店里都得有现货。HotSpot源代码里,StubRoutines的生成逻辑分布在stubRoutines.cpp、stubGenerator_xx.cpp(xx是平台后缀,比如stubGenerator_x86_64.cpp)等文件里,启动时由StubRoutines::initialize2()统一驱动生成。
2.3 为什么不用C++函数,非要自己生成机器码?
这是StubRoutines最核心的设计动机,也是理解它的钥匙。理论上,JVM里的这些固定操作(比如“把参数从解释器栈搬到C++调用约定”),完全可以写一个C++函数,Java代码调JNI的时候走一遍函数调用不就行了?但现实是:
- 调用约定不同。Java方法调用使用的是JVM自己的栈帧约定(比如解释器栈),而C++函数使用平台ABI(比如System V AMD64或Windows x64)。两者之间需要一个“翻译层”,把参数、返回值、栈指针在两种约定之间来回切换。
- 性能极度敏感。这个翻译层是每次Java方法调用、每次异常抛出都可能经过的路径,如果还用普通函数一层层嵌套,开销直接爆炸。
- 有些操作根本没法用C++写。比如“生成一段代码,它把自己的地址压栈后跳转到另一个地址”,这种底层控制流的活,只能直接在机器码层面拼。
所以HotSpot在启动时
