如何在启动JVM前跨平台获取JNI/JVM版本?
关于JNI_GetDefaultJavaVMInitArgs的用法问题
你的代码没有语法错误,但对该API的行为理解存在偏差:
这个API的设计目标是获取指定JNI版本规范对应的默认JVM启动参数,而非主动探测当前JVM的实际版本。调用时你需要传入期望兼容的JNI版本号,只要当前JVM支持你传入的版本(所有现代JVM都向下兼容到JNI 1.2),函数就会返回JNI_OK,且不会修改你传入的version字段。你在Java 9环境下传入JNI_VERSION_1_2(值为0x10002),返回后version字段保持原值是完全符合API预期的行为,不是调用失败。
跨平台JVM启动前获取版本的可行方案
以下方案均不需要启动JVM,可在Windows、Linux、macOS全平台通用:
方案1:JNI版本号试探法(兼容性最高,无依赖)
JNI版本号的编码规则是固定的:
- Java 8及更早版本:版本号格式为
0x0001000<次版本号>,比如Java 8对应JNI_VERSION_1_8即0x10008 - Java 9及之后版本:版本号格式为
0x00<主版本号十六进制>0000,比如Java 9对应0x90000、Java 17对应0x110000、Java 21对应0x150000,每升级一个主版本号,数值加0x10000
你可以从已知的最高JNI版本开始,从高到低依次传入JNI_GetDefaultJavaVMInitArgs试探,第一个返回JNI_OK的版本就是当前JVM支持的最高JNI版本,直接对应JVM主版本。示例代码:
#include <string.h> #include <jni.h> // 如果编译用的JNI头文件版本较低,没有高版本宏定义,直接写对应十六进制值即可 #ifndef JNI_VERSION_9 #define JNI_VERSION_9 0x00090000 #endif #ifndef JNI_VERSION_11 #define JNI_VERSION_11 0x000b0000 #endif #ifndef JNI_VERSION_17 #define JNI_VERSION_17 0x00110000 #endif #ifndef JNI_VERSION_21 #define JNI_VERSION_21 0x00150000 #endif jint get_jvm_version_before_init() { jint test_versions[] = { JNI_VERSION_21, JNI_VERSION_17, JNI_VERSION_11, JNI_VERSION_9, JNI_VERSION_1_8, JNI_VERSION_1_6, JNI_VERSION_1_4, JNI_VERSION_1_2 }; JavaVMInitArgs tmp_args; memset(&tmp_args, 0, sizeof(tmp_args)); for (int i = 0; i < sizeof(test_versions)/sizeof(test_versions[0]); i++) { tmp_args.version = test_versions[i]; jint ret = JNI_GetDefaultJavaVMInitArgs(&tmp_args); if (ret == JNI_OK) { return test_versions[i]; } } return JNI_VERSION_1_2; // 兜底返回最低兼容版本 }
拿到返回的版本号后,就可以判断JVM主版本,拼接对应版本适配的JVM参数,再正式调用JNI_CreateJavaVM启动JVM即可。
方案2:JVM库符号/元数据读取法(性能最高)
加载对应平台的JVM动态库(Windows下为jvm.dll、Linux下为libjvm.so、macOS下为libjvm.dylib)后,直接读取库的版本信息:
- Windows:调用系统
GetFileVersionInfo系列API读取dll的版本资源,直接获取JDK完整版本号 - Linux/macOS:通过
dlsym查找HotSpot虚拟机导出的JDK_Version::current()符号(所有OpenJDK、Oracle JDK等HotSpot系发行版从Java 6到最新版本均稳定导出该符号),调用后可直接获取主、次、修订版本号
该方案不需要重复调用API试探,执行效率最高,但需要适配不同平台的系统API,对非HotSpot实现的JVM(比如GraalVM Native Image嵌入场景、IBM J9等)兼容性稍差。
注意:网传“一个进程只能启动一次JVM、销毁后无法重启”是过时结论:只要正确调用
DestroyJavaVM完成所有资源释放、完全卸载JVM动态库,是可以重新启动JVM实例的。如果对启动速度不敏感,也可以启动一个不带任何自定义参数的最小临时JVM,调用GetVersion拿到准确版本后销毁,再用适配好的参数正式启动JVM,只是该方案会有数百毫秒的额外启动开销。
内容的提问来源于stack exchange,提问作者Michael Anderson

