Appearance
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 编译器在独立的 Compile Daemon 进程里跑,它只继承 Gradle Daemon 的
- 为什么 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")
}- 可能执行顺序
- Gradle 触发 Kotlin 编译任务。
- 若已有 Daemon 的 JVM 参数集合与请求一致 → 复用;否则启动新 Daemon 实例。
- 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.jvmargsvskotlin.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=-Xmx2g2. 大仓里给「重编译模块」单独大内存、其余保持默认
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 侧生效」。
复习检查题
Kotlin Compile Daemon 从 Gradle Daemon 继承哪几个 JVM 参数?其余参数怎么处理? 答:只继承
-Xmx、-XX:MaxMetaspaceSize、-XX:ReservedCodeCacheSize(未指定则默认 320m)。其余参数(如 GC 选择)不继承,Daemon 套自己的默认值(如-XX:+UseParallelGC)。想给 Kotlin 编译器调 GC 或加堆内存,正确写在哪里? 答:写
gradle.properties的kotlin.daemon.jvmargs,或在build.gradle.kts的kotlin { kotlinDaemonJvmArgs = ... };堆内存也可经 Gradle Daemon 的-Xmx继承,但 GC 必须走 daemon 专有入口。Kotlin 2.0 默认的编译器是什么?如何回退到旧编译器? 答:默认 K2(FIR 前端)。回退用
-language-version 1.9或在kotlinOptions.languageVersion设为 1.9。为什么不同子项目配了不同
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 参数,别让子项目各自为政。