Skip to content

Kotlin 编译守护进程、jvmargs 继承与 K2 ​

回到总览:构建与发布工程相关模块:Gradle 生命周期与 AGP 任务链(其中「三类缓存」提到 Gradle Daemon,本文补全 Kotlin 专用的 Compile Daemon)

一句话定义 ​

Kotlin 编译器跑在独立进程 Kotlin Compile Daemon 里,避免每次编译重启 JVM、复用编译环境以加速增量构建。它只从 Gradle Daemon 继承 3 个 JVM 参数,其余参数自己套默认值——所以你在 org.gradle.jvmargs 里配的 GC、调优参数对 Kotlin 编译器基本不生效。Kotlin 2.0 起默认启用 K2 编译器,编译更快、增量更稳。

代码索引 ​

本主题为 Gradle / Kotlin 编译器配置知识,无需可运行 lab;以 gradle.properties 片段说明即可。

为什么需要 ​

  • 为什么中型以上项目改一行 Kotlin 代码,编译还是慢、甚至 OOM,而我在 org.gradle.jvmargs 里加的 -Xmx / GC 参数好像没起作用?
    • 一句话答:Kotlin 编译器在独立的 Compile Daemon 进程里跑,它只继承 Gradle Daemon 的 -Xmx / -XX:MaxMetaspaceSize / -XX:ReservedCodeCacheSize 三个值;你在 Gradle 侧配的 GC、其余 JVM 参数都被忽略,Daemon 套自己的默认(如 -XX:+UseParallelGC)。
  • 为什么 Kotlin 2.0 之后编译明显变快?
    • 一句话答:K2 编译器用新前端(FIR)重构,类型检查与解析更快、增量编译更细;Koin 等库官方实测 koin-core-iosArm64 编译时间下降约 69%。
  • 为什么 CI 上 Kotlin 编译偶发「换台机器就慢/挂」?
    • 一句话答:Compile Daemon 按「JVM 参数集合」决定是否复用已有进程;不同机器 / 不同子项目 JVM 参数不一致会启动多个 Daemon 实例,内存叠加;且 daemon 默认 2 小时空闲才退出,可能堆积吃内存。

1. 实现原理 ​

1. Compile Daemon 只继承 3 个 JVM 参数 ​

Gradle Daemon 启动 Kotlin Compile Daemon 时,仅透传以下三者;其余全部重置为 Daemon 内置默认值:

继承(来自 Gradle Daemon)不继承(Daemon 用自己默认)
-Xmx(堆)-XX:+UseG1GC 等任何 GC 选择
-XX:MaxMetaspaceSize-ea、-XX:+UseCodeCacheFlushing 等
-XX:ReservedCodeCacheSize(未指定则默认 320m)-Djava.awt.headless=true、JDK16+ 的 --add-exports

Daemon 自身默认参数(节选):-XX:+UseParallelGC(未指定其他 GC 时)、-ea、-XX:+UseCodeCacheFlushing、-Djava.awt.headless=true。

关键结论:想在 Gradle 侧给 Kotlin 编译器加堆内存,只有 -Xmx 等 3 个能透传;想调 GC 或其余 JVM 参数,必须走下面专有的 kotlin.daemon.jvmargs。

2. 正确的配置入口:kotlin.daemon.jvmargs ​

要调 Kotlin 编译器本身的 JVM 参数,写在 gradle.properties:

properties
# gradle.properties
kotlin.daemon.jvmargs=-Xmx1500m -Xms500m
# 注意:若此处和 Gradle 侧都没写 ReservedCodeCacheSize,插件会补默认 320m:
#   -Xmx1500m -XX:ReservedCodeCacheSize=320m -Xms500m

也可在 build.gradle.kts 的 kotlin 扩展里写(更贴近模块):

kotlin
kotlin {
    kotlinDaemonJvmArgs = listOf("-Xmx1500m", "-Xms500m", "-XX:+UseParallelGC")
}
  • 可能执行顺序
    1. Gradle 触发 Kotlin 编译任务。
    2. 若已有 Daemon 的 JVM 参数集合与请求一致 → 复用;否则启动新 Daemon 实例。
    3. Daemon 用「继承的 3 个值 + 自己的默认 + kotlin.daemon.jvmargs 追加项」启动 JVM 并编译。
  • 预期现象:用 jps -v / VisualVM 观察 Daemon 进程参数,应看到 -Xmx 来自继承或 kotlin.daemon.jvmargs,而 GC 是 UseParallelGC(除非你在 kotlin.daemon.jvmargs 显式指定 G1/ZGC)。
  • 观察重点:不同子项目若配了不同 kotlinDaemonJvmArgs,会并存多个 Daemon 进程、各自占内存——大仓要权衡「按需大内存」与「进程数膨胀」。

3. K2 编译器与增量 ​

Kotlin 2.0 起 kotlinOptions.languageVersion 默认 2.0,K2(FIR 前端)成为默认编译器。回退旧编译器用 -language-version 1.9。增量编译依赖 Build Cache 与稳定的配置输入;Compile Daemon 的复用减少 JVM 冷启动,与 Build Cache 正交、可叠加。

Android / Flutter / Web / Backend 对照 ​

  • Android / Kotlin:Compile Daemon 是 Kotlin 编译的常驻进程,直接影响 assemble* 的增量速度;GC、堆调优走 kotlin.daemon.jvmargs。
  • Flutter / Dart:Dart 无独立「编译守护进程」概念,AOT/JIT 由 flutter 工具链直接驱动;此主题仅作对照,不必深究。
  • Web / Backend:JVM 后端的 Kotlin 编译同样受 Compile Daemon 规则约束;Maven 也可配(kotlin.daemon.jvm.options),思路一致。

2. 特点与优势 ​

  • Gradle Daemon vs Kotlin Compile Daemon:前者托管构建系统(跑 task),后者托管 Kotlin 编译器;两者是「构建进程」与「编译进程」的嵌套关系,参数继承只有 3 个。
  • org.gradle.jvmargs vs kotlin.daemon.jvmargs:前者管 Gradle 自身 JVM,几乎不影响 Kotlin 编译器(仅 3 个值透传);后者专管 Compile Daemon。
  • Compile Daemon vs Build Cache:Daemon 省 JVM 冷启动,Build Cache 省任务产物;两者正交、可叠加(详见 Gradle 生命周期 的「三类缓存」)。

3. 用法 ​

1. 给 Kotlin 编译单独加堆内存(避免 OOM) ​

properties
# gradle.properties
kotlin.daemon.jvmargs=-Xmx2g

2. 大仓里给「重编译模块」单独大内存、其余保持默认 ​

kotlin
// 仅在个别模块的 build.gradle.kts
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
    if (project.name == "heavy-module") {
        kotlinDaemonJvmArgs = listOf("-Xmx4g", "-Xms2g")
    }
}

常见误配、事故后果与排障 ​

1. 误配:在 org.gradle.jvmargs 里配 GC,期望加速 Kotlin 编译 ​

  • 现象:编译慢 / GC 停顿没改善,查 Daemon 参数发现仍是 UseParallelGC。
  • 排障与修法:GC 与绝大多数 JVM 参数不被 Compile Daemon 继承;改放到 kotlin.daemon.jvmargs 或 kotlin { kotlinDaemonJvmArgs = ... }。

2. 事故:CI 多 Daemon 实例内存叠加被 OOM Kill ​

  • 误配原因:不同子项目 JVM 参数不一致,触发多个 Daemon 并存,内存叠加超过容器限额。
  • 后果:编译进程被系统 OOM Kill,流水线随机失败。
  • 排障与修法:统一全仓 kotlin.daemon.jvmargs,避免子项目各自为政;容器限额内预留 Daemon 内存;必要时用 kotlin.daemon.jvm.options=autoshutdownIdleSeconds=... 缩短空闲存活。

3. 误配:-Xmx 设得比 Gradle Daemon 还小却期望继承 ​

  • 排障与修法:-Xmx 是唯一能从 Gradle Daemon 继承的值;若你想让 Kotlin 编译器用更大堆,必须在 kotlin.daemon.jvmargs 显式写大值,Gradle 侧的小 -Xmx 不会自动放大。

对应实验 ​

计划补齐的实验:

  • labs/android/host/topics/build/kotlin-daemon-jvmargs-demo/ — 在 gradle.properties 配 kotlin.daemon.jvmargs,用 jps -v 观察 Daemon 实际参数,验证「Gradle 侧 GC 不继承、daemon 侧生效」。

复习检查题 ​

  1. Kotlin Compile Daemon 从 Gradle Daemon 继承哪几个 JVM 参数?其余参数怎么处理? 答:只继承 -Xmx、-XX:MaxMetaspaceSize、-XX:ReservedCodeCacheSize(未指定则默认 320m)。其余参数(如 GC 选择)不继承,Daemon 套自己的默认值(如 -XX:+UseParallelGC)。

  2. 想给 Kotlin 编译器调 GC 或加堆内存,正确写在哪里? 答:写 gradle.properties 的 kotlin.daemon.jvmargs,或在 build.gradle.kts 的 kotlin { kotlinDaemonJvmArgs = ... };堆内存也可经 Gradle Daemon 的 -Xmx 继承,但 GC 必须走 daemon 专有入口。

  3. Kotlin 2.0 默认的编译器是什么?如何回退到旧编译器? 答:默认 K2(FIR 前端)。回退用 -language-version 1.9 或在 kotlinOptions.languageVersion 设为 1.9。

  4. 为什么不同子项目配了不同 kotlinDaemonJvmArgs 可能导致 CI OOM? 答:Daemon 按「JVM 参数集合」决定是否复用;参数不一致会并存多个 Daemon 实例,各自占内存叠加,超过容器限额即被 OOM Kill。

速记 ​

  • 继承只有 3 个:-Xmx / -XX:MaxMetaspaceSize / -XX:ReservedCodeCacheSize(默认 320m)。
  • 调编译用专有入口:kotlin.daemon.jvmargs(或 kotlinDaemonJvmArgs),别塞进 org.gradle.jvmargs。
  • K2 默认开:Kotlin 2.0 起 K2 编译器,编译更快、增量更细。
  • 多 Daemon 会吃内存:统一全仓 daemon JVM 参数,别让子项目各自为政。

站点构建时间:2026/8/24 23:43:17