ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

深入 JVM 源码:从 abstract_vm_version.cpp 看版本号是怎么来的

2026/10/5 16:16:00 拓冰建站 浏览量
深入 JVM 源码:从 abstract_vm_version.cpp 看版本号是怎么来的 把 DeepSeek 这类大模型当成“代码导航员”去啃 OpenJDK HotSpot 源码我选中的第一个文件就是hotspot/share/runtime/abstract_vm_version.cpp。这个文件名乍一看有点劝退又是 abstract 又是 vm_version但读完之后你会发现JVM 启动时打印的那行版本号、java -version输出的每一个字段、甚至-Xlog查到的内部信息全都从这个文件里来。如果你跟我一样喜欢刨根问底这篇文章就把这个文件从成员变量到方法实现从编译期宏到运行时调用链完整拆给你看。我会结合近期用 DeepSeek 做代码检索和注释的实践把这个源码文件背后的工程价值也一并说清楚。文章面向的是有一定 C/C 基础、想深入 JVM 源码的读者如果你只想做业务开发看完至少能理解“版本号不匹配”这种报错到底发生在哪一层。1. abstract_vm_version.cpp 在 HotSpot 里到底管什么1.1 文件定位与职责边界先明确一件事这个文件不在 JDK 的src/java.base里而在 HotSpot 虚拟机的 C 源码目录src/hotspot/share/runtime/下。HotSpot 本身是 C 写的它自己有一套版本管理和运行时自检机制和 Java 标准库完全是两码事。abstract_vm_version.cpp对应的头文件是abstract_vm_version.hpp。它定义了一个名为Abstract_VM_Version的类这个类承担了三大块职责维护 JVM 自身的版本号主版本、次版本、安全版本、补丁级别、构建号。生成供外部展示的版本字符串比如java -version里那三行内容。提供 CPU 特性检测、平台相关初始化的抽象底座真正的平台特化逻辑由子类VM_Version完成不同架构各有一份比如cpu/x86/vm_version_x86.cpp。要理解这个类的存在意义可以把它类比成程序里的“全局配置中心”。JVM 在启动早期就必须知道自己是哪个版本、跑在什么 CPU 上、支持哪些指令集扩展这样才能决定 JIT 编译器能不能用 AVX2、要不要启用 SHA 指令加速等。版本号看似只是给人看的实际上也参与运行时的行为分支判断。1.2 为什么叫 abstract抽象底座与平台子类的关系在 HotSpot 源码演进过程中原先的vm_version.hpp/cpp逐步被拆成了abstract_vm_version和VM_Version两层。抽象层存放与平台无关的版本信息和方法子类则补齐 CPU 探测、指令集特性等平台相关内容。打个比方这就像一套房屋设计图纸抽象层加上针对不同地基的施工方案平台子类。图纸规定“必须有楼板和承重墙”施工方案决定“南方用空心砖、北方用红砖”。Abstract_VM_Version里有很多方法声明成虚函数或者由子类覆盖比如class Abstract_VM_Version: public AllStatic { // ... static void initialize(); static const char* vm_release(); static const char* vm_version_string(); static const char* internal_vm_info_string(); static void print_version(); static void initialize_cpu_information(); // ... };AllStatic意味着这个类没有实例成员全是静态的本质上是一组全局函数和全局变量的集合。这种写法在 HotSpot 的底层工具类里很常见因为它需要被任何模块随时调用不需要持有对象状态。2. 核心成员与方法实现拆解2.1 版本号从哪来编译期宏与静态成员版本号不是写死在代码里的字符串而是由构建系统生成宏定义再在源码里引用。这个过程和大多数 C/C 项目的build_version.h如出一辙。在 JDK 构建时make系统会计算当前版本信息并生成jdk/src/hotspot/share/runtime/abstract_vm_version.hpp能看到的宏典型的有#define VERSION_MAJOR 17 #define VERSION_MINOR 0 #define VERSION_SECURITY 2 #define VERSION_PATCH 0 #define VERSION_BUILD 8对应到Abstract_VM_Version的静态成员初始化int Abstract_VM_Version::_vm_major_version VERSION_MAJOR; int Abstract_VM_Version::_vm_minor_version VERSION_MINOR; int Abstract_VM_Version::_vm_security_version VERSION_SECURITY; int Abstract_VM_Version::_vm_patch_level VERSION_PATCH; int Abstract_VM_Version::_vm_build_number VERSION_BUILD;为什么不用一个字符串完事因为版本号经常需要参与数值比较和位运算。比如jvm_version()这个方法要把版本号压缩进一个unsigned intunsigned int Abstract_VM_Version::jvm_version() { return (_vm_major_version 24) | (_vm_minor_version 16) | (_vm_security_version 8) | _vm_patch_level; }这样17.0.20就变成了0x11020000这样的数值。JVM 内存管理、GC 日志、服务端管理接口需要快速判断“当前 JVM 是否大于某个版本”时直接比较整数比解析字符串快得多也可靠得多。2.2 版本字符串的拼装过程vm_version_string()是最终呈现在java -version里的核心方法。它的实现不是简单拼接数字而是根据构建类型和平台信息动态组装。源码里大致逻辑如下const char* Abstract_VM_Version::vm_version_string() { if (_vm_version_string NULL) { char* tmp NEW_C_HEAP_ARRAY(char, 100, mtInternal); if (is_openjdk()) { jio_snprintf(tmp, 100, OpenJDK %d.%d.%d_%d, _vm_major_version, _vm_minor_version, _vm_security_version, _vm_patch_level); } else { jio_snprintf(tmp, 100, Java HotSpot(TM) %d.%d.%d_%d, _vm_major_version, _vm_minor_version, _vm_security_version, _vm_patch_level); } _vm_version_string tmp; } return _vm_version_string; }注意这里首次调用才分配内存后续直接返回缓存指针这是 HotSpot 里很常见的懒加载模式。因为这串字符串会被反复打印到日志和输出流没必要每次都重新格式化。在较新的 JDK 版本里字符串格式已经调整成与java -version保持一致。比如标准输出看起来是这样的openjdk version 17.0.2 2022-01-18 OpenJDK Runtime Environment (build 17.0.28-86) OpenJDK 64-Bit Server VM (build 17.0.28-86, mixed mode, sharing)第一行来自java.base模块的 Java 代码而第二行其实是调用 JVM 的internal_vm_info_string()得到的结果从 C 层传递回去再打印的。所以当你怀疑某个构建版本不对时需要同时排查 Java 层和 JVM 层两套版本号。2.3 与 JDK 版本体系的关系从 JDK 9 开始JVM 版本号和 JDK 版本号基本对齐但依旧保留两套查询接口jvm_version()返回 HotSpot 内部版本号。jdk_version()返回 JDK 版本号。两者的区别在发布周期里会体现出来。JDK 版本是“产品版本”而 JVM 版本是“实现版本”。举个实际例子JDK 17.0.2 对应的 JVM 内部版本经常是17.0.28其中8是构建号同一产品版本在不同发行方那里的构建号可能不同。jdk_version()的实现大致如下unsigned int Abstract_VM_Version::jdk_version() { unsigned int jdk_version (VERSION_MAJOR 24) | (VERSION_MINOR 16) | (VERSION_SECURITY 8) | (VERSION_PATCH); // 如果 patch 不为 0还要把 patch 编码进低 8 位 return jdk_version; }这种位布局是为了让外部工具可以通过位掩码快速提取版本分量避免解析字符串。3. 编译期宏与构建系统的版本串联3.1 从 make 到宏的生成链路如果你第一次看 HotSpot 构建过程会被它那套层层嵌套的 makefile 绕晕。版本宏的生成链路大致是make/autoconf/version-numbers定义基础版本号 →configure读取并生成configure-output→make阶段写入jdk/make/data下的版本属性 → 最终生成 C 头文件宏或 Java 属性文件。在构建日志里能看到类似这样的输出openjdk version 17.0.2 2022-01-18 OpenJDK Runtime Environment (build 17.0.28-86)这里的8是构建号-86是构建元数据通常包含构建机器标识或源码变更集。所有这一切都会传导到Abstract_VM_Version的静态成员里。所以如果你改了版本宏文件只需重新编译 HotSpot 即可生效但如果只是清理了build目录而没有重新运行configure版本信息和实际源码可能对不上。这是很多自编译 JDK 的人最容易踩的坑。3.2 CPU 特性检测的初始化流程Abstract_VM_Version里有一块非常值得读的代码initialize()方法。它负责在 JVM 启动早期探测当前 CPU 支持哪些特性然后记录在_feature_flags之类的静态位掩码里。以 x86 平台为例VM_Version::initialize()会调用__cpuid()指令拿到 CPUID 信息然后检查各种特性位比如 AVX、AVX2、AES-NI、SSE4.2 等。这些检测结果之后会被 JIT 编译器使用决定能否生成特定指令。void VM_Version::initialize() { // 调用 cpuid 指令 // 设置 _features 位掩码 // 根据特性决定是否启用某些优化 if (supports_avx2()) { // 设置 AVX2 相关标志 } // ... }有个很有意思的细节CPU 特性探测必须在 JVM 启动很早期完成因为后续线程创建、内存分配可能都会依赖这些信息。如果某些指令集不支持还强行使用会导致SIGILL非法指令崩溃。所以 HotSpot 宁可先探测后使用也绝不猜测。3.3 修改版本号后如何重新构建研究源码时很多人会想改个版本号试试。如果你只想改本地构建的显示版本不需要改太多地方。以 OpenJDK 17 为例先把make/autoconf/version-numbers里的版本号改掉DEFAULT_VERSION_FEATURE17 DEFAULT_VERSION_INTERIM0 DEFAULT_VERSION_UPDATE2 DEFAULT_VERSION_PATCH0 DEFAULT_VERSION_BUILD8然后删掉build目录重新配置构建。注意只改这里还不够src/java.base/share/classes/java/lang/VersionProps.java.template里也维护了一套 JDK 版本信息两个地方必须保持一致否则会出现java -version和内部 API 查询结果不一致的诡异现象。提示如果你只是做源码研究不要在生产环境的 JDK 上瞎改版本号。HotSpot 的显式版本管理在多个模块JVMCI、管理接口、服务代理里都有引用版本号不一致轻则导致 JFR 记录出错重则触发断言失败直接拒绝启动。4. 运行时调用链从 java 命令到源码输出4.1java -version到底走了哪几层很多人以为java -version只是打印环境变量实际上它的调用链跨了 Java 和 C 两层。大致的调用过程是java可执行程序C 语言写的 launcher启动加载 JVM 动态库。解析参数时发现-version调用JNI_GetCreatedJavaVMs等 JNI 函数获取 JVM 实例。JVM 初始化过程中调用Abstract_VM_Version::vm_version_string()生成版本信息字符串。launcher 拿到这段字符串后再结合java.base模块里的VersionProps类信息打印出完整的三行内容。所以你会看到即使没有进入任何 Java 代码JVM 的 C 层已经在打印信息了。这一点在调试启动很慢的 JVM 时特别有用——只要版本能打出来说明 JVM 库加载和基础初始化已经完成。4.2 内部信息字符串与诊断日志internal_vm_info_string()是比vm_version_string()更详细的一段字符串通常包含 JVM 模式client/server/mixed、调试级别、编译器配置等内容。它的输出在 JVM 崩溃时会出现在hs_err_pid*.log的头部# JRE version: OpenJDK Runtime Environment (17.0.2) (build 17.0.28-86) # Java VM: OpenJDK 64-Bit Server VM (17.0.28-86, mixed mode, tiered, compressed oops, g1 gc)这些信息正是由 JVM 的版本管理模块输出的。当你把崩溃日志发给别人排查时对方第一眼看的也是这段内容因为它直接说明了你在用什么 JVM、什么 GC、什么编译模式。4.3 用-Xlog观察版本信息现代 JDK 支持统一日志标签我们可以直接从命令行观察版本信息初始化过程java -Xlog:vmversioninfo -version不过更常见的是用java -Xlog:runtimeinfo -version输出里能看到类似下面的记录[info][runtime] Version: 17.0.28-86 [info][runtime] CPU: x86_64, 8 cores, family 6 model 154这背后的数据来源正是Abstract_VM_Version维护的静态成员和 CPU 检测结果。如果 CPU 型号识别不对可以去查VM_Version的平台实现看看是不是_cpu字段解析出了问题。5. 常见问题与排查技巧实录5.1 版本号对不上定位思路在实际运维或开发中最常见的情况是java -version显示的版本和实际运行的版本不一致。很多人第一反应是环境变量PATH有问题这当然要先查。但还有一种场景同一个 JVM 进程里Java API 查到的版本和java -version命令查到的版本不一致。这种问题我排查过一次最后发现是系统里安装了两个不同构建的 JDK一个放在JAVA_HOME另一个被 launcher 优先加载。真正的版本号要基于 JVM 内部Abstract_VM_Version的值因为 launcher 只是“引路人”JVM 库才是“本人”。可以在代码里用Runtime.version()或者通过管理接口查jcmd pid VM.version对比输出就能判断 JVM 实际加载的是哪一份库。5.2 自编译 JDK 的版本信息陷阱自编译 JDK 的人经常遇到一个问题编译出来的 JVM 在java -version里显示的不是自己改的版本号而是默认的internal版本。原因大多出在make/autoconf/version-numbers和VersionProps.java.template没有同步。另一个坑是改了源码里VERSION_MAJOR之类的宏但没有清理旧的中间文件构建系统自认为无需重新编译于是旧的版本号残留在.o文件里。解决办法很简单直接make clean后再构建。如果你用的是增量构建至少也要保证abstract_vm_version.o被重新编译。实战经验我在研究 HotSpot 时就吃过这个亏。改完宏后只执行了make images结果版本号纹丝不动折腾了一个多小时才发现是预编译头文件缓存的问题。后来养成了习惯改动任何与版本相关的宏第一时间make clean。5.3 版本信息对 JIT 行为的影响Abstract_VM_Version里还有一组静态方法比如supports_cpu_features()和cpu_features()。它们返回的位掩码不仅用于日志输出还在 JIT 编译流程中作为开关。举个例子UseAVX这个 JVM 参数会和 CPU 检测到的特性做“按位与”运算得出实际可用的指令集级别。如果你在老的 CPU 上强行设置-XX:UseAVX3JVM 启动时就会给出 warning并自动回退到安全级别。这种容错设计保证了同一个 JVM 二进制可以在不同年代的 CPU 上运行。这一块对于想理解“为什么我的 JVM 没有启用 AVX2”的读者尤其有价值——排查路径不是看参数而是先看 CPU 检测结果。6. 从源码管理到工程实践的个人体会6.1 DeepSeek 在同一类问题上的工程启示标题里带上了 DeepSeek我读这个文件时也确实习惯性地用 DeepSeek 来帮我快速定位方法、解释宏定义。有意思的是DeepSeek 这类大模型的 C 推理引擎里也有非常类似的版本管理模块模型版本、tokenizer 版本、kernel 版本、CUDA 适配层版本全都是运行时自检的一部分。一个追求稳定的底层系统永远需要一套“我是谁、我在哪、我能干什么”的自省机制。HotSpot 的做法是把版本信息拆成数值和字符串两层并通过编译期宏注入DeepSeek 的做法则是把模型版本和引擎版本分开管理。两者的共同点是宁可多存几份元信息也要确保运行时能在最短时间内回答“版本匹配吗、特性支持吗”。这种“多一层自省少一次崩溃”的思路放在任何复杂的 C 项目里都适用。6.2 从这段源码学到的可复用经验读abstract_vm_version.cpp最大的收获不是某个具体 API而是几个工程习惯版本号必须可被程序解析而不仅是给人看。所以用整数位运算而不是字符串比较。版本信息初始化要早要幂等要懒加载。HotSpot 把所有版本字符串的生成都推迟到第一次使用时避免启动路径上不必要的开销。平台差异需要抽象层兜底。“抽象父类 平台子类”的组合在 JVM 源码里无处不在版本管理也遵循这个模式。如果我在自己的项目里设计类似的运行时自检模块一定会参考 HotSpot 的做法定义一组不可变的静态常量提供格式化输出接口同时保留原始数值入口供程序判断。这比在业务代码里到处拼字符串优雅太多了。最后再分享一个实际建议如果你准备啃 HotSpot 源码别一上来就看 GC 或 JIT那些代码量太大容易劝退。先从abstract_vm_version.cpp这种“小而完整”的文件入手配合java -Xlog观察真实输出一周内就能建立起对 JVM 启动流程的整体认知。等你能不看注释讲清楚版本字符串是从哪几层代码拼出来的再回头看其他模块会轻松很多。