ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从vm_version_zero.cpp看HotSpot如何适配新CPU架构

从vm_version_zero.cpp看HotSpot如何适配新CPU架构 第一次看到jdk-jdk-27-6/src/hotspot/cpu/zero/vm_version_zero.cpp这个路径大部分人都会愣一下jdk-jdk-27-6是什么版本cpu/zero又是哪个 CPU等你真正把文件翻出来大概率还会再愣一次——它竟然只有几十行而且从头到尾看不到一条 CPU 指令。可就是这么个“不起眼”的小文件卡在 HotSpot 虚拟机最尴尬的位置上没有它整个 JDK 在非主流架构上根本链接不过去。今天我想从这行路径聊起把 HotSpot 的 CPU 移植模型、Zero 解释器的设计逻辑以及这个看似“空实现”的文件真正承担的任务一次讲清楚。适合正在读 JDK 源码、做交叉编译、或者好奇 JVM 如何适配新 CPU 的朋友内容偏源码层但我会尽量用大白话拆开揉碎。1. 拿到这个路径先搞清楚它属于 HotSpot 的哪一层1.1 src/hotspot 为什么按 cpu、os、os_cpu 分目录很多人第一次翻 OpenJDK 源码时都会在src/hotspot底下迷路。这个目录不是简单按功能分而是按“跨平台能力”做了三层隔离。第一层是share存放跟 CPU、操作系统都无关的代码。比如类加载、垃圾回收、JIT 编译器的中后端、常量池解析、字节码解释器框架这些逻辑在 x86 上跑和在 AArch64 上跑应该是同一套。第二层是cpu/arch专门处理跟芯片指令集强相关的东西。真正需要产机器码的模块比如模板解释器、JIT 编译器、栈帧生成、原子操作封装都会在这里分目录。第三层是os/os和os_cpu/os_arch前者管信号、线程、文件系统这类操作系统抽象后者管“操作系统 芯片”组合出来的具体行为比如不同平台上如何做栈回溯、如何设置线程上下文。cpu/zero就在第二层。但有趣的是它不像cpu/x86、cpu/aarch64那样对应某一种实际芯片而是一个“虚拟 CPU 移植”。也就是说HotSpot 官方目录里专门留了一个位置给那些还没有正式移植、甚至连 JIT 后端都懒得写的架构使用。vm_version_zero.cpp正是这个虚拟移植里最靠前的一块拼图。1.2 jdk-jdk-27-6 到底是哪个版本标题里的jdk-jdk-27-6看起来像“两段 jdk 拼在一起”其实对应的是 OpenJDK 主线仓库里某个早期访问构建。官方 Mercurial 仓库中一个 build 的 tag 通常会写成jdk-276表示 JDK 27 的第 6 个集成构建在一些镜像仓库、源码包目录或者 IDE 工程里加号会被转写成短横线就成了你看到的jdk-27-6。前一个jdk一般代表组织或仓库名后一个则是目录名的一部分。所以不必被这个路径吓到它不是某个神秘分支而是 JDK 27 开发过程中的一个快照版本。这个版本里 HotSpot 仍然保留了cpu/zero目录说明 Zero 移植没有被清理掉。你在读这段源码时可以先把版本概念放一边把它当成“OpenJDK 主线在 27 这个周期里的某一刻”即可。真的要确认版本可以看仓库根目录的make/autoconf/version-numbers或者编出来后直接跑java -version。2. Zero 移植一个不产生机器码的 HotSpot“备胎”2.1 为什么 Java 虚拟机会有一个 Zero 端口要理解vm_version_zero.cpp必须先理解Zero本身。Zero 这个词来自 Zero Assembler意思不是“零”而是“零汇编代码”。它最早源自 IcedTea 项目目的是让 OpenJDK 能在那些还没有对应 HotSpot JIT 移植的 CPU 架构上跑起来。OpenJDK 官方对主流平台的支持非常完善比如 x86、AArch64、PPC64LE、s390x都有完整的模板解释器和 C2 JIT 后端。但新出现的芯片架构想跑 JDK原本需要等整个 JIT 后端移植完成这个工作量非常大。Zero 的解决思路非常“偷懒”我不生成任何机器码全部用 C 写一个解释器字节码到行为的映射就是一堆switch分支。只要你的平台上能跑 C 编译器理论上就能编译出一个可用的 JVM。代价是性能很一般因为它放弃了 JIT、放弃了模板解释器带来的指令级优化所有 Java 方法都靠解释执行。但它解决了一个关键问题新架构不需要再等到 JIT 后端写完就能先把整个 JDK 跑起来做验证。这类移植非常适合做三件事一是 CPU 厂商验证自家新架构能不能跑 Java二是研究 JVM 语义的人读源码三是一些极度冷门的嵌入式平台想要“能用就行”的 JDK。我当年研究一块 RISC-V 开发板时就是先靠 Zero 把 JDK 跑起来后面才慢慢看汇编层支持的。2.2 zero 目录里的其他“零”文件vm_version_zero.cpp并不是孤军奋战。打开src/hotspot/cpu/zero目录你会看到一堆名字里带zero的文件它们共同组成了一套完整的解释执行引擎。简单说有负责栈帧处理的、有负责字节码跳转的、有负责运行时调用约定的、还有负责原子操作的。这些文件全都遵循同一个原则不直接写目标平台的汇编指令而是借助 C 编译器能力来落地。其中vm_version_zero.cpp是“发布信息”的角色。HotSpot 启动时很多模块都会来问“当前 CPU 有哪些指令集扩展、有没有 64 位 CAS、要不要走慢速原子路径”这些问题统一由 VM_Version 这个类回答。在 x86 上这个类会调用cpuid指令把 CPU 特性寄存器翻个底朝天在 AArch64 上它会去读系统寄存器但在 zero 目录里它几乎什么都不读直接给出一组最保守的答案。这不代表这个文件没用恰恰相反正是因为它给出了“最安全答案”整个 HotSpot 才能在不同架构上保持统一的启动流程。2.3 和模板解释器的本质区别有人会把 Zero 移植理解成“一个简化版 JVM”其实不太准确。HotSpot 默认的解释器叫模板解释器虽然也叫解释器但它并不是一条字节码一条字节码直接解释的。它在初始化时会给每条字节码生成一段机器码模板之后执行时直接跳到对应模板地址用生成的本地代码完成操作。这个过程需要架构相关的汇编代码因此每个平台都要单独维护。Zero 就简单得多解释器主循环就是一段 C 代码拿到字节码后switch到对应的处理函数整体逻辑和你在《自己动手写 Java 虚拟机》这类书里看到的小型解释器很像。它不需要在运行时为字节码生成机器码所以也就不需要关心指令编码、寻址模式、寄存器分配这些头疼的问题。vm_version_zero.cpp正是这套“不关心指令集”思路的集中体现连 CPU 特性检测都不做一键返回空集。3. vm_version_zero.cpp 到底做了什么一个“空实现”的巧妙设计3.1 VM_Version 是 HotSpot 的 CPU 身份证在展开源码之前可以先打个比方。你买电脑时会看 CPU 天梯图知道这颗芯片支持哪些指令集、有没有超线程、缓存多大然后决定这台机器能干什么。JVM 也需要类似的“天梯图”只不过它的用途更底层C2 JIT 编译器在生成代码前必须知道 CPU 支持 SSE 还是 AVX、支持 AES 指令还是不支持不然一编译出来就是非法指令崩溃垃圾回收器也要知道你有多大缓存行来决定某些设计参数原子操作模块更想知道你支不支持 64 位无锁比较交换因为这会直接影响锁性能。这份“天梯图”就是VM_Version类。每个 CPU 目录下的vm_version_arch.cpp都会实现这个类把当前 CPU 的能力填好供虚拟机其他模块随时查询。而vm_version_zero.cpp的特别之处在于它填出来的表几乎是空的。因为你根本不需要生成任何机器码JIT 也没有模板解释器也没有指令集特性毫无意义。但 HotSpot 的启动流程是平台无关的它不管你在哪个 CPU 上跑都会去调用VM_Version::initialize()如果你不提供实现链接阶段直接报错。所以这个文件的第一价值是“占位”让整个 HotSpot 的编译链接流程保持完整。3.2 打开文件后看到的逻辑一段简化版我手头没有 JDK 27 的完整源码逐字稿但基于 OpenJDK 里 Zero 移植多年来的稳定逻辑这个文件的内容基本长这样。为了让你看清核心我写一个可读性更好的简化版本真实源码里就是多了一些头文件包含和平台宏思路完全一致// src/hotspot/cpu/zero/vm_version_zero.cpp #include precompiled.hpp #include runtime/arguments.hpp #include runtime/vm_version.hpp void VM_Version::initialize() { // Zero 不生成任何机器码因此没有可上报的 CPU 特性。 _features 0; // 64 位平台默认支持 8 字节 CAS可以减少通用代码的锁开销。 _supports_cx8 true; // 以下两个标志让原子操作模块走“快速且安全”的通用路径。 _supports_atomic_getset4 true; _supports_atomic_getset8 true; _supports_atomic_getadd4 true; _supports_atomic_getadd8 true; }代码很短短到有人觉得这是个没写完的文件。但请仔细想想一个 JVM 解释器实现里VM_Version::initialize()最合理的实现就该长这样。它不需要读寄存器不需要执行cpuid不需要维护一堆 CPU 特性位。_features 0不是“没实现”而是“有意为之”——既然没有 JIT任何指令集扩展状态都是多余的。需要明确一点_supports_cx8 true这行不是拍脑袋写的。HotSpot 的通用代码在很多地方会问“当前平台支不支持 64 位 compare-and-swap”如果回答不支持原子操作就会退化成加锁实现性能很难看。Zero 移植面向的实际平台几乎都是 64 位系统它们都满足 8 字节 CAS 的存在条件。把它直接置为 true是为了让上层代码走高效路径。如果将来某个 32 位或奇葩架构也想接 Zero这行通常需要根据实际能力调整。3.3 几个关键“空”函数存在的意义除了initialize()vm_version_zero.cpp里可能还会实现一些看似多余的函数比如打印平台虚拟化信息、返回 CPU 描述字符串、输出特性列表。它们看起来都是空壳但每一个空壳都在维护一个跨平台契约。举个例子JVM 在 crash 或者-version输出时会尝试显示 CPU 特性字符串。x86 上你会看到avx512, vmx, aes之类一长串Zero 上可能只会返回一个空字符串或者固定的zero。其他模块调用它时拿到的值虽然是“空”但调用行为本身被完整保留了。这有点像插座标准不管插座后面有没有电器墙上必须预留那个接口否则整栋楼的电路设计就崩了。vm_version_zero.cpp干的就是这件事。从另一个角度看这个文件也是“接口隔离原则”在大型项目里的一个范例。HotSpot 的共享代码只需要知道VM_Version这个类有哪些方法不需要关心具体平台如何实现。新增一个 CPU 移植时你只要把这几个方法补齐其他模块就能无缝工作。这就是为什么你能在cpu/aarch64看到几百行的vm_version_aarch64.cpp也能在cpu/zero看到几十行的vm_version_zero.cpp它们功能差异极大但对外接口保持一致。3.4 为什么不干脆删掉它也许你会想既然 Zero 不需要 CPU 特性为什么不在共享代码里写一个默认实现然后让所有平台继承问题没那么简单。HotSpot 对 VM_Version 的调用点很多有的平台还有些额外钩子比如返回缓存行大小、返回是否支持某些内存屏障指令。如果把这些全部塞到共享代码会让“平台无关层”变得越来越不干净。反而是现在这种“每个 cpu 目录都必须提供一份实现”的方式简单粗暴但足够清晰——新平台维护者打开目录就知道要做什么。所以vm_version_zero.cpp的“空”不是空洞无物而是经过权衡后的最小实现。它把“不确定”变成“确定”把“平台的多样性”收敛到一组统一接口里。很多读过这份代码的人都会有个感受真正的复杂度不在怎么写而在于知道什么时候应该不写。4. 亲自动手编译一个 Zero 版本的 JDK并观察它的产物4.1 准备源码和工具链读源码是一回事把 Zero 版 JDK 真正编出来又是另一回事。我建议你按下面的流程走一遍只有自己构建过才会明白vm_version_zero.cpp在整个 JDK 里处于什么位置。首先拿到源码。你可以从 OpenJDK 官方 GitHub 镜像下载主线代码也可以找到对应jdk-276的 tag 检出来。如果你看到的是jdk-jdk-27-6这样的目录大概率是某个镜像已经帮你解压好了直接使用即可。源码体积不小磁盘上最好预留 10 GB 以上空间编译过程中中间产物很大。然后准备 boot JDK。OpenJDK 不能凭空编译自己它需要一个“引导 JDK”来驱动构建脚本和早期类库编译。JDK 27 的构建通常要求一个足够新的 JDK 作为 bootconfigure 阶段会自动检测如果找不到或版本不匹配它会明确告诉你需要哪个版本。不要想跳过这一步老老实实先装一个正常的发行版 JDK 在本地。最后确认工具链。Linux 上一般需要gcc、g、make、autoconf等基础工具。零版本不需要目标平台汇编器这个说法基本只适用于 HotSpot 本身JDK 其他本地代码组件仍然依赖完整工具链所以别指望只装一个编译器就能一键完成。4.2 configure 和 make 的关键参数进入源码根目录后先创建一个构建目录然后执行 configure。Zero 最重要的参数就是--with-jvm-variantszero它会告诉构建系统这一轮只构建解释执行的 HotSpot VM不要默认的 server 变体。bash configure \ --with-jvm-variantszero \ --with-boot-jdk/path/to/your/boot/jdk \ --disable-warnings-as-errors--disable-warnings-as-errors是我强烈建议加上的。因为新版本 GCC/Clang 经常会报出一些旧源码里的编译告警OpenJDK 默认把这些告警当成错误处理导致构建失败。这个参数并不会削弱产出质量只是把“告警即失败”的策略关掉对理解源码没有任何影响。configure 通过后直接执行make images构建时间取决于机器性能通常在十几分钟到一小时不等。等它跑完到build/*/jdk/bin/下看一眼你会找到一个java可执行文件。build/*/jdk/bin/java -version如果一切正常输出里会明确带着Zero VM字样。看到这句话时你刚才读过的那个几十行的小文件就通过编译链接实实在在地进入了你的libjvm动态库里。4.3 验证 vm_version_zero.cpp 确实参与了构建构建完成只是第一步我建议你再做两个小实验加强印象。第一个实验是看符号。VM_Version::initialize这个函数在最终产物里是存在的而且可以被工具找到。用nm或objdump查看build/*/jdk/lib/server/libjvm.so然后过滤VM_Version相关符号你会看到类似_ZN9VM_Version10initializeEv这样的 C mangled 符号。这正是vm_version_zero.cpp被编译进 HotSpot 的直接证据。第二个实验是跑一个最小 Java 程序echo public class HelloZero { public static void main(String[] args) { System.out.println(Hello from Zero VM); }} HelloZero.java build/*/jdk/bin/javac HelloZero.java build/*/jdk/bin/java HelloZero程序能正常输出就说明 Zero 版 JDK 的语义完整性没有问题。你还可以用-XX:PrintFlagsFinal看看默认参数和标准 JDK 的差异里面很多和 JIT 相关的选项会被标记为不支持这正是因为你编译的是一个没有 JIT 后端的虚拟机。5. 常见问题与避坑记录5.1 configure 卡在 “A valid boot JDK was not found”这个报错几乎每个人都会碰到。原因很简单--with-boot-jdk指向的路径不对或者路径里的 JDK 版本不符合要求。JDK 27 的构建通常需要较新的 JDK 作为引导你拿一个很老的 JDK 8 去试肯定会失败。解决方案是先去安装一个官方发布的、较新的 JDK然后在 configure 里明确指定路径。注意--with-boot-jdk要指向 JDK 的安装根目录也就是包含bin/java的那一层不是指向bin目录。如果还不行就删掉 build 目录重新 configure因为 OpenJDK 的 configure 有时会缓存旧路径。5.2 编译到一半因为告警失败开发版 JDK 源码编译时最容易遇到的就是gcc新版本把某个告警升级成了 hard error。这种情况不用慌在 configure 时加上--disable-warnings-as-errors再重来一次即可。我见过有人担心关闭这个选项会编译出“不安全的 JDK”其实不会。它只是不把编译告警当阻断项而不是关闭告警本身。真正要关心的还是源码逻辑而不是编译器措辞。如果编译仍然失败再看具体报错多半是缺少依赖库或者内核头文件按提示安装即可。5.3 运行 java -version 非常慢或者很多选项不可用Zero 版 JDK 毕竟是纯解释执行性能远不如正常版。有人编译完跑java -version发现启动时间比标准 JDK 长这是正常现象不是编错了。因为没有 JIT很多启动阶段的热点优化都享受不到感觉“肉”是在预期之内的。另外像-XX:PrintAssembly、-Xcomp、-server这类跟 JIT 强相关的参数在 Zero VM 里基本不可用或会被忽略。如果你只是想验证代码语义保持默认的-Xint解释模式就好。想深入观察解释器状态可以加-XX:UnlockDiagnosticVMOptions配合诊断参数但不要指望有汇编输出。5.4 新架构接入 Zero 时第一件事是改 vm_version_zero.cpp如果你想把自己手里的新芯片跑起来除了交叉编译工具链外最需要关注的就是vm_version_zero.cpp里的能力声明。这个文件虽然小但它决定上层代码对这个平台的“预期”。比如你的新平台可能不支持 64 位原子 CAS或者原子操作需要特判那你就要在这里把对应的_supports_*标志调成 false让上层代码降级到更保守的实现。同样的道理如果平台支持某些快速路径比如比较交换、内存屏障也要在这里明确。很多人以为 Zero 移植就是改改解释器主循环其实第一步往往是先把这个“身份证”办明白否则后面各种莫名其妙的问题都会出现有时是死锁有时是变量更新不一致根因只是平台能力标志没配对。5.5 我和这个文件打过交道的几点体会我在一块非常冷门的 ARM 开发板上试过 Zero 版 JDK当时最深的感受是越是这种不起眼的文件越能反映一个大型项目的设计底线。HotSpot 能在几十种 CPU、操作系统组合上保持一致行为靠的不是把每个细节都写成高深算法而是把“平台差异”牢牢关进诸如cpu/zero这样的目录里。vm_version_zero.cpp用几十行代码告诉你有些时候最好的实现就是明确说什么都不需要。后来我再读 x86 的vm_version_x86.cpp理解速度明显快了很多因为我脑子里已经有了一张“契约清单”这个类是给谁看的、要回答哪些问题、哪些平台选择保守、哪些平台选择激进。反过来再有人问怎么给新架构做最小移植我也总会先带他看这个最不起眼的 zero 版本文件因为它把 HotSpot 的接口需求暴露得最干净。
返回列表