PAYATHON 2026

为什么本地 Windows 构建后需要 JVMCI compiler?

排障赵师傅

结论

jvmcicompiler.dll 是特定 JDK 或 GraalVM 发行版中的 JVMCI 编译器组件,普通项目通常不会直接依赖它。程序启动时如果出现:

Cannot use JVMCI compiler: No JVMCI compiler found

说明 JVM 已启用 JVMCI 编译器,但应用捆绑的 Java 运行时中没有对应实现。

常见原因有两个:

  1. 本地启动参数或环境变量中设置了 -XX:+UseJVMCICompiler。
  2. 构建时使用了不同的 JDK 发行版,生成的运行时镜像不完整或与应用不匹配。

不要把 jvmcicompiler.dll 单独复制到构建产物中。应删除不需要的 JVMCI 参数,或者让构建任务使用并打包完整、匹配的 JDK 运行时。

为什么相同代码和 Gradle 会生成不同结果

Gradle 支持可复现构建,但构建结果仍可能受环境影响。如果任务读取了本机 JDK、环境变量、绝对路径或未声明的外部文件,不同环境就可能得到不同的输出。

排查时需要逐项核对:

  • Java 主版本是否相同
  • JDK 厂商和发行版是否相同
  • CPU 架构是否相同
  • Gradle Daemon 实际使用哪个 JDK
  • IntelliJ IDEA 为 Gradle 配置了哪个 JDK
  • jlink、jpackage 或打包插件使用了哪个 JDK

即使两个环境都显示 Java 21,它们提供的运行时组件也可能不同。标准 OpenJDK、Oracle JDK、GraalVM 及其他厂商发行版,对 JVMCI 的支持和附带文件可能存在差异。

如果构建产物中带有自己的 java/bin/java.exe,项目很可能生成或复制了捆绑运行时。编译 Java 源码的 JDK 和生成运行时镜像的 JDK 也可能不是同一个。

先检查 JVMCI 参数来自哪里

先在 PowerShell 中搜索项目配置:

Get-ChildItem -Recurse -File |
    Select-String -Pattern 'UseJVMCICompiler|EnableJVMCI|JVMCI|jvmcicompiler'

重点查看这些位置:

  • build.gradle、build.gradle.kts
  • gradle.properties
  • 启动脚本
  • IntelliJ IDEA Run Configuration
  • applicationDefaultJvmArgs
  • jlink 或 jpackage 配置
  • 项目使用的运行时打包插件配置

还要检查可能向所有 Java 进程注入参数的环境变量:

'JAVA_HOME','JDK_JAVA_OPTIONS','JAVA_TOOL_OPTIONS','_JAVA_OPTIONS' |
    ForEach-Object {
        [PSCustomObject]@{
            Name  = $_
            Value = [Environment]::GetEnvironmentVariable($_)
        }
    }

如果其中包含:

-XX:+UseJVMCICompiler

而项目不需要 Graal 编译器,就应删除这个参数。下载的 release 能在同一台机器上运行,说明全局环境变量不太可能是唯一原因。不过仍有必要检查,因为两个启动脚本处理环境变量的方式可能不同。

org.gradle.jvmargs 通常只控制 Gradle Daemon,不应直接传给应用。如果构建脚本把这组参数复制到了应用启动器,也会引发同类问题。

确认 Gradle 实际使用的 JDK

运行以下命令:

.\gradlew --version
.\gradlew javaToolchains
where.exe java
java -version
java -XshowSettings:properties -version

与发布构建环境对照,检查:

  • java.vendor
  • java.version
  • java.home
  • os.arch
  • Gradle JVM 路径

还可以查看捆绑运行时的版本:

.\path\to\built\java\bin\java.exe -version
Get-Content .\path\to\built\java\release

对下载的 release 执行相同检查。与只看 java -version 相比,比较 release 文件通常更容易发现发行版或模块差异。

不需要 JVMCI 时的修改方法

大多数普通 Java 应用无须显式启用 JVMCI。如果构建脚本中有类似配置,可以将其删除:

application {
    applicationDefaultJvmArgs = listOf(
        "-XX:+UseJVMCICompiler"
    )
}

改为不包含该选项:

application {
    applicationDefaultJvmArgs = emptyList()
}

如果配置中还有 -XX:+EnableJVMCI,需要确认项目是否确实依赖 JVMCI API。JDK 支持 JVMCI,不等于应用必须启用它。

修改后停止旧的 Gradle Daemon,再重新构建:

.\gradlew --stop
.\gradlew clean
.\gradlew build

Gradle Daemon 会长期保留启动时使用的 JDK 和部分环境信息,所以切换 JAVA_HOME 后最好先执行 --stop。

固定构建使用的 JDK

可以通过 Gradle Java Toolchains 固定 Java 版本和厂商。Kotlin DSL 示例:

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(21))
        vendor.set(JvmVendorSpec.ADOPTIUM)
    }
}

Groovy DSL 示例:

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
        vendor = JvmVendorSpec.ADOPTIUM
    }
}

示例中的版本和厂商需要替换成项目发布流程实际使用的值。

同时要确认运行时打包任务是否采用了该 Toolchain。部分 jlink、jpackage 或第三方打包插件有独立的 javaHome、jdkHome、launcher 或 toolchain 配置,只设置 java.toolchain 未必能控制这些任务。可用的具体属性取决于项目使用的插件。

确实需要 JVMCI 时怎么办

如果应用明确依赖 Graal 编译器或其他 JVMCI 编译器,应使用项目指定的完整 JDK 或 GraalVM 发行版,并让打包任务保留所需的模块和原生库。

不宜长期采用单独复制 jvmcicompiler.dll 的做法,因为该文件可能还依赖:

  • 同一发行版中的其他动态库
  • 特定 Java 补丁版本
  • 对应的 JVMCI 模块
  • 相同的 CPU 架构
  • 匹配的 JVM 启动配置

单个 DLL 即使能让当前机器正常启动,也无法保证生成的发行包在其他机器或 JDK 升级后仍能正常运行。

为什么错误会出现三次

同一错误连续出现三次,通常是因为启动器尝试启动了多个 Java 进程,或者在失败后进行了重试。例如,主程序、辅助进程和后台服务都可能读取同一组 JVM 参数。

这不表示系统缺少三个不同的 DLL。可以在启动脚本中打印最终命令行,或使用进程监控工具确认 java.exe 实际启动了多少次。

推荐的排查顺序

  1. 搜索并删除不必要的 -XX:+UseJVMCICompiler。
  2. 检查 JAVA_TOOL_OPTIONS、JDK_JAVA_OPTIONS 和 _JAVA_OPTIONS。
  3. 比较本地构建与 release 中的 java/release 文件。
  4. 用 gradlew --version 和 gradlew javaToolchains 确认实际使用的 JDK。
  5. 固定 JDK 的版本、厂商和架构。
  6. 检查运行时打包插件是否另外选择了 JDK。
  7. 停止 Gradle Daemon,清理后重新构建。
  8. 项目确实需要 JVMCI 时,再打包完整且匹配的 JVMCI 运行时。

备注:内容仅供参考。