主题
第 9 章 · 性能优化:让 Gradle 飞起来 🚀
学习目标:搞懂 Daemon、Build Cache、Configuration Cache、并行执行、Worker API 等机制, 能用
--scan/--profile找瓶颈,把构建从 2 分钟优化到 20 秒。
1. 为什么必须搞性能优化?
来算一笔账:
- 一个中型 Spring Boot 项目,全量构建一次需要 90 秒
- 一个开发者一天构建 30 次(改完代码 → run)
- 30 个开发者 → 一天浪费在构建上的总时间 =
90 × 30 × 30 / 3600 = 22.5 小时 - 一个月 → 等于白白损失 3 个人天 💸
如果把这 90 秒优化到 20 秒,每个月就多出 2 个人天的有效产出。
这就是为什么大厂都有专门的"构建工程"小组,Gradle 性能优化是核心 KPI。
2. 性能优化全景图
⚡ Gradle 性能优化金字塔 ⚡
┌────────────────────┐
│ 🔴 极端优化 │ Worker API / Lazy task
│ (定制化) │
└────────────────────┘
┌────────────────────────────┐
│ 🟠 Configuration Cache │ 二次构建配置阶段省到 0
│ (Gradle 7.4+ 默认推荐) │
└────────────────────────────┘
┌──────────────────────────────────┐
│ 🟡 Build Cache(任务级缓存) │
│ 本地 + 远端,跨开发者复用产物 │
└──────────────────────────────────┘
┌────────────────────────────────────────┐
│ 🟢 Incremental Build(增量) │
│ 声明 inputs/outputs,让 task 自己判断 │
└────────────────────────────────────────┘
┌────────────────────────────────────────────────┐
│ 🟢 Parallel + Daemon(并行 + 守护进程) │
│ 开关一翻就能加速,零成本 │
└────────────────────────────────────────────────┘记住一个金字塔原则:先把下面的基础打开(几乎零成本),再考虑上层的高阶优化。
3. Daemon:常驻后台的 Gradle 进程
没有 Daemon 时
你执行 ./gradlew build
→ 启动 JVM (3-5s)
→ 加载 Gradle 类(数千个 class)
→ 解析 build script
→ 执行任务
→ 退出,JVM 销毁
下一次 ./gradlew build → 重复以上所有步骤!有 Daemon 时
第 1 次构建:
→ 启动 Daemon (JVM 3-5s + 类加载)
→ 把 Daemon 留在后台
→ 执行任务
第 2 次起:
→ 直接复用 Daemon ⚡(省了启动 + 类加载)
→ 配置脚本类也已 JIT 优化实测数据
| 场景 | 冷启动(首次) | 热启动(已有 Daemon) |
|---|---|---|
./gradlew help | 6-8 秒 | 0.5-1 秒 |
./gradlew clean build | 90 秒 | 60 秒 |
Daemon 配置
gradle.properties:
properties
# 默认就是 true,无需手动开
org.gradle.daemon=true
# 给 Daemon 多少内存?大项目至少 2G
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError
# 多久没活儿就退休?默认 3 小时
# 单位是毫秒,下面是 30 分钟
org.gradle.daemon.idletimeout=1800000常用 Daemon 命令
bash
# 看现在有几个 Daemon 在跑
./gradlew --status
# 杀掉所有 Daemon(构建死锁/卡住时用)
./gradlew --stop
# 这次不用 Daemon(CI 上偶尔会用)
./gradlew --no-daemon build💡 生活类比:Daemon 就像外卖小哥的电瓶车。 没有它 → 每送一单都要从公司步行出发; 有了它 → 第一单先骑出门,后续直接接单不用回去取车。
4. Parallel:并行编译子模块
properties
# gradle.properties
org.gradle.parallel=true原理
Gradle 能识别"互不依赖的子项目",把它们的 task 调度到不同线程执行。
子模块 A (3s) ──┐
子模块 B (4s) ──┼─ 串行:3 + 4 + 5 = 12s
子模块 C (5s) ──┘
子模块 A (3s) ──┐
子模块 B (4s) ──┼─ 并行:max(3, 4, 5) = 5s
子模块 C (5s) ──┘注意
- 默认 worker 数 = CPU 核数
- 可手动指定:
org.gradle.workers.max=4 - 不依赖的同模块内 task也能并行(前提是声明了 inputs/outputs)
💡 生活类比:餐厅厨房里 3 个厨师同时炒不同菜,比一个人挨个炒快多了。
5. Build Cache:任务级缓存
核心思想
Gradle 把每个 task 的输入指纹(hash)记下来。 下次构建时,如果 hash 不变,直接从缓存里取上次的输出,连任务都不执行!
[假设 task :compileJava]
输入:所有 .java 源文件 + JDK 版本 + 编译参数
↓ 算出 hash
sha256: 9f4a...8b2c
↓ 查 build cache
找到 → ⚡ 直接复制对应的 .class 文件出来
找不到 → 真正执行编译,把结果存入缓存启用
properties
# gradle.properties
org.gradle.caching=true本地 vs 远程
| 缓存类型 | 配置位置 | 用途 |
|---|---|---|
| Local Cache | ~/.gradle/caches/build-cache-1/ | 自己机器上反复重建/切分支秒回 |
| Remote Cache | settings.gradle.kts 中配置 HTTP server | 团队共享:CI 编译过一次,全员都不用编了 |
远程缓存配置示例
kotlin
// settings.gradle.kts
buildCache {
local {
isEnabled = true
}
remote<HttpBuildCache> {
url = uri("https://gradle-cache.example.com/cache/")
isPush = System.getenv("CI") == "true" // ★ 只允许 CI 推
credentials {
username = providers.gradleProperty("cacheUser").get()
password = providers.gradleProperty("cachePass").get()
}
}
}哪些 task 能被缓存?
- 内置 task(
compileJava、test、jar、compileKotlin等)开箱即用 - 自定义 task 必须打
@CacheableTask注解 + 正确声明@InputXxx/@OutputXxx
一键见效演示
bash
./gradlew clean build --no-build-cache # 模拟"没缓存"
# Output: 90s
./gradlew clean build --build-cache # 启用缓存
# Output: 12s ← 8 倍提速!💡 生活类比:你做了一道番茄炒蛋,味道好就拍照存到云相册。 下次想吃,直接看照片下单外卖,不用再炒一遍。
6. Configuration Cache:配置阶段也能缓存
这是 Gradle 7.4 之后最受期待的特性,但需要插件支持,请小心启用。
它解决什么?
回忆第 3 章的生命周期:每次执行都会走配置阶段(即使 task 已增量), 配置阶段评估所有 build.gradle.kts,可能耗时 5~20 秒。
Configuration Cache:把"配置阶段产出的 task graph"序列化到磁盘。 下次构建只要 settings/build script 没改,直接跳过配置阶段,从 task graph 反序列化。
提速效果
| 场景 | 普通 | + Build Cache | + Configuration Cache |
|---|---|---|---|
./gradlew help(再次) | 1.5s | 1.5s | 0.3s |
./gradlew test(增量) | 8s | 4s | 0.8s |
启用
properties
# gradle.properties
org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn # 出问题先警告而不是失败或临时使用:
bash
./gradlew build --configuration-cache风险与限制
⚠️ Configuration Cache 要求构建脚本"行为可序列化",因此:
- 在 task 配置块里直接调用
System.getenv()→ 会被警告 - 在
doLast/@TaskAction里调用 → 没事 - 老插件可能不兼容(看
build/reports/configuration-cache/报告)
kotlin
// ❌ 不友好:配置阶段读环境变量
tasks.register("bad") {
val env = System.getenv("FOO")
doLast { println(env) }
}
// ✅ 推荐:用 Provider API
val envProvider = providers.environmentVariable("FOO")
tasks.register("good") {
doLast { println(envProvider.orNull) }
}7. Worker API & Incremental Task
写自定义 task 时,要榨出极限性能,请用 Worker API + Incremental Task。
Worker API 的价值
把任务里的"重活儿"丢给独立的 Worker 进程并行执行:
kotlin
abstract class CompressImagesTask : DefaultTask() {
@get:InputDirectory
abstract val inputDir: DirectoryProperty
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@get:Inject
abstract val workerExecutor: WorkerExecutor
@TaskAction
fun compress() {
val workQueue = workerExecutor.noIsolation()
inputDir.asFileTree.forEach { file ->
workQueue.submit(CompressAction::class.java) {
input.set(file)
output.set(outputDir.file(file.name).get().asFile)
}
}
// 不需要手动 wait,Gradle 会等所有 work 完成
}
}100 张图压缩,单线程要 50 秒,Worker API 8 线程并行 → 7 秒搞定。
Incremental Task
只处理"上次构建后变化过的"输入文件,第 5 章已介绍过 @Incremental + InputChanges。
8. 排查工具:到底卡在哪?
Build Scan(最强工具,强烈推荐)
bash
./gradlew build --scan执行后会输出一个 URL,浏览器打开后能看到:
- 每个 task 的耗时排行
- Configuration / Execution 阶段时间分布
- 缓存命中率
- 依赖下载情况
- 警告与失败原因
--profile 离线版
bash
./gradlew build --profile
# 输出:build/reports/profile/profile-*.html--info / --debug
bash
./gradlew build --info # 中等详细
./gradlew build --debug # 海量日志,调依赖问题用看 task 是否被缓存命中
> Task :compileJava FROM-CACHE # ⚡ 缓存命中
> Task :compileJava UP-TO-DATE # ⚡ 增量未变
> Task :compileJava # 真正执行了
> Task :compileJava NO-SOURCE # 没有源文件,跳过9. 优化检查清单 ✅
入手任何项目,先把这 7 项过一遍:
- [ ]
gradle.properties设置org.gradle.daemon=true - [ ]
gradle.properties设置org.gradle.parallel=true - [ ]
gradle.properties设置org.gradle.caching=true - [ ]
gradle.properties设置org.gradle.configuration-cache=true(先=warn) - [ ]
gradle.properties设置org.gradle.jvmargs=-Xmx2g(防 OOM) - [ ] 所有自定义 task 都声明
@InputXxx/@OutputXxx - [ ] 团队搭建 Remote Build Cache(CI + 共享)
把这套做完,绝大多数项目能拿到 3-10 倍提速。
10. 真实优化案例:从 5 分钟到 38 秒
某团队 30+ 模块的 Java 项目,初始全量构建 5 分 12 秒。
| 步骤 | 措施 | 耗时 | 节省 |
|---|---|---|---|
| 0 | 原始(什么都没开) | 5:12 | — |
| 1 | 开 daemon + parallel | 3:40 | -1:32 |
| 2 | 开 build cache(本地) | 2:15 | -1:25 |
| 3 | 自定义 task 都补 inputs/outputs | 1:50 | -0:25 |
| 4 | 远程 cache(CI 推送) | 1:05 | -0:45 |
| 5 | 配置缓存 + 干掉慢插件 | 0:38 | -0:27 |
结论:8 倍速度提升,全部靠"开关 + 规范",没动一行业务代码。
11. 面试题与陷阱题 🎯
Q1:Gradle Daemon 是什么?为什么默认开启?
答:Daemon 是常驻后台的 Gradle 进程,避免每次构建都启动 JVM 和加载所有类。默认开启因为 JVM 启动 + 类加载本身就要 3-5 秒,Daemon 省下这部分开销,在迭代频繁的开发场景能显著加速。
Q2:org.gradle.parallel 为什么不默认开?
答:并行能加速但有副作用:
- 老的、不严谨的 task 可能依赖了"全局共享状态",并行下出现竞争
- 资源密集的 task 一并行可能 OOM
- 输出日志会交错,调试不便
所以 Gradle 选择"默认关闭、推荐你开"。
Q3:Build Cache 和 UP-TO-DATE 有什么区别?
答:
UP-TO-DATE:本次构建判断输入没变,直接跳过 task 执行FROM-CACHE:本地或远程缓存里有同 hash 的输出,直接拷贝出来
UP-TO-DATE 是单次构建内的判定;Build Cache 是跨构建、甚至跨机器复用。
Q4:Configuration Cache 的局限是什么?
答:要求所有 task 配置阶段不能:
- 直接读
System.getenv()/System.getProperty() - 直接读
project.exec的结果 - 在配置块里访问其他 project(违反"项目隔离")
替代方案是用 providers.environmentVariable()、providers.gradleProperty() 这类懒求值 API。
Q5:怎么判断一个 task 是否值得开缓存?
答:3 条标准:
- 输入/输出都能精确声明(不能依赖外部不可控因素)
- 同输入永远能产出相同输出(确定性)
- 输出文件较大、计算耗时较长(缓存才划算)
满足全部 → 加 @CacheableTask。
Q6:Worker API 比直接开线程好在哪?
答:
- 自动接入 Gradle 的并行调度(
max-workers) - 支持 ProcessIsolation(进程隔离),用得安全
- 异常会聚合到 Gradle 报告里,定位方便
- 进度信息会显示在终端 / Build Scan 上
Q7:构建慢,用什么命令排查?
答:
--scan上传到 gradle.com 看可视化报告--profile生成本地 HTML 报告--info看 task 决策原因gradle --status看 daemon 是否健康
Q8:远程 Build Cache 一般怎么部署?
答:可选方案:
- Gradle Enterprise:官方商用方案,性能最好
- Develocity Build Cache Node:免费版,自托管
- 简单 HTTP Server:参考社区方案如 gradle-cache-node
策略:开发者只读,CI 才能写 → 防止本地脏构建污染缓存。
Q9:什么情况下应该禁用 daemon?
答:
- CI 上的一次性短任务(保留 daemon 反而占内存)
- 调试 daemon 自身问题
- 切换 Gradle 版本验证差异
CI 通常用 --no-daemon 或者 org.gradle.daemon=false。
Q10:JVM 参数 -Xmx 应该设多少?
答:经验值:
- 小型项目(<10 模块):1G 即可
- 中型(10-50 模块):2-4G
- 大型(50+ 模块、含 Android/Kotlin):6-8G
设太大无意义(GC 压力反而上升),太小会 OOM Daemon。 观察方法:构建中 jstat -gc <daemon-pid> 1s 看堆使用情况。
📌 本章小结
优化口诀:先开关,再规范,最后定制。
一句话:daemon + parallel + caching + configuration-cache 是四件套, 缺一不可,开了就能让 90% 的 Java/Kotlin 项目获得明显提速。
下一章我们将把前 9 章学到的所有知识揉到一个实战项目里,从零到一搭一个真实的多模块工程。
🎬 可视化演示
演示加载缓慢或样式异常?点此在新标签页打开 ↗
💻 示例代码
txt
/*
* 第 9 章 · build.gradle.kts
* ------------------------------------------------------------
* 演示:
* 1) 给自定义 task 加 @CacheableTask 让它能进 Build Cache
* 2) Worker API 的并行执行
* 3) Configuration Cache 安全的 task 写法(用 Provider,不直接读 env)
*/
plugins {
java
}
java {
toolchain { languageVersion.set(JavaLanguageVersion.of(17)) }
}
repositories { mavenCentral() }
// ============================================================
// 示例 1:可缓存的"行数统计" task
// ============================================================
@CacheableTask
abstract class CountLinesTask : DefaultTask() {
@get:InputDirectory
@get:org.gradle.api.tasks.PathSensitive(org.gradle.api.tasks.PathSensitivity.RELATIVE)
abstract val sourceDir: org.gradle.api.file.DirectoryProperty
@get:Input
abstract val fileExtension: org.gradle.api.provider.Property<String>
@get:OutputFile
abstract val report: org.gradle.api.file.RegularFileProperty
@TaskAction
fun count() {
val ext = fileExtension.get()
val total = sourceDir.asFileTree.matching {
include("**/*." + ext)
}.sumOf { it.readLines().size }
report.get().asFile.writeText("总行数(*.$ext)=$total\n")
logger.lifecycle("✅ 行数统计完成:$total(已写入缓存友好型 task)")
}
}
tasks.register<CountLinesTask>("countLines") {
group = "perf"
description = "可缓存:第二次运行如果输入未变会 FROM-CACHE"
sourceDir.set(layout.projectDirectory.dir("src"))
fileExtension.set("java")
report.set(layout.buildDirectory.file("reports/lines.txt"))
}
// ============================================================
// 示例 2:演示"Configuration Cache 友好"的写法
// ============================================================
// ❌ 错误示范(会被 Configuration Cache 警告)
// tasks.register("badEnv") {
// val env = System.getenv("USER") // 在配置阶段直接读,破坏可缓存性
// doLast { println("Hello, $env") }
// }
// ✅ 正确示范:用 Provider 懒求值
val userEnv = providers.environmentVariable("USER")
tasks.register("greet") {
group = "perf"
description = "Configuration Cache 友好版"
doLast {
println("👋 Hello, ${userEnv.orNull ?: "anonymous"}")
}
}
// ============================================================
// 示例 3:Worker API 并行处理多个文件
// ============================================================
interface MockWorkParameters : org.gradle.workers.WorkParameters {
val index: org.gradle.api.provider.Property<Int>
}
abstract class MockWorkAction : org.gradle.workers.WorkAction<MockWorkParameters> {
override fun execute() {
val i = parameters.index.get()
// 模拟一个耗时 600ms 的"压缩/转换"操作
Thread.sleep(600)
println(" worker-${Thread.currentThread().id} 完成任务 #$i")
}
}
abstract class ParallelDemoTask : DefaultTask() {
@get:javax.inject.Inject
abstract val workerExecutor: org.gradle.workers.WorkerExecutor
@TaskAction
fun go() {
val queue = workerExecutor.noIsolation()
val total = 8
logger.lifecycle("派发 $total 个 worker 任务...")
repeat(total) { i ->
queue.submit(MockWorkAction::class.java) {
index.set(i)
}
}
logger.lifecycle("派发完毕,等待 worker 收尾。")
}
}
tasks.register<ParallelDemoTask>("parallelDemo") {
group = "perf"
description = "Worker API 并行示例:8 个任务,并行 4-8 个 worker,1s 内完成"
}
// ============================================================
// 示例 4:观察哪些 task 会跑 / 哪些会 UP-TO-DATE
// ============================================================
tasks.register("explainCache") {
group = "perf"
description = "提示如何观察缓存命中"
doLast {
println("""
▶ 试试以下命令观察输出:
./gradlew countLines # 第一次:真正执行
./gradlew countLines # 第二次:UP-TO-DATE
./gradlew clean countLines --build-cache
# 第三次:FROM-CACHE
▶ 想看慢在哪儿?
./gradlew build --scan
./gradlew build --profile
""".trimIndent())
}
}txt
# 第 9 章 · 性能优化推荐配置 · gradle.properties
# 把这份配置直接复制到任何项目,立刻拿到 3-5 倍提速。
# === 1. Daemon ===
org.gradle.daemon=true
# 30 分钟没活儿就退(默认 3 小时太久)
org.gradle.daemon.idletimeout=1800000
# === 2. 并行 ===
org.gradle.parallel=true
# 默认是 CPU 核数,一般无需改;超大项目可设为 8/16
# org.gradle.workers.max=8
# === 3. Build Cache ===
org.gradle.caching=true
# === 4. Configuration Cache(Gradle 7.4+ 推荐) ===
# 先用 warn 模式探探,看哪些插件不兼容
org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn
# === 5. JVM 参数 ===
# 大项目至少 2g,避免 Daemon OOM
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# === 6. 只重新评估发生变化的项目(按需配置阶段) ===
org.gradle.configureondemand=true
# === 7. Kotlin 编译加速 ===
kotlin.incremental=true
kotlin.incremental.useClasspathSnapshot=true
# === 8. 文件监听(Gradle 6.5+) ===
org.gradle.vfs.watch=truemarkdown
# 第 9 章 · 性能优化代码示例
## 文件清单
| 文件 | 看点 |
|------|------|
| `gradle.properties` | 性能优化"全开"模板,可直接复制到任何项目 |
| `settings.gradle.kts` | 演示如何配置本地 + 远程 Build Cache |
| `build.gradle.kts` | 4 个示例:可缓存 task / Configuration Cache 友好写法 / Worker API / 调试技巧 |
| `src/main/java/Sample.java` | 给 countLines 任务用的占位源 |
## 推荐操作步骤
```bash
# 1) 看 daemon 状态
./gradlew --status
# 2) 第一次运行(真正执行)
./gradlew countLines
# 3) 第二次运行(UP-TO-DATE,秒回)
./gradlew countLines
# 4) clean 后再来一次(这次走 FROM-CACHE)
./gradlew clean countLines --build-cache
# 5) Worker API 并行:观察输出里 worker-id 是不同的线程
./gradlew parallelDemo
# 6) 强制 Configuration Cache 模式跑一次
./gradlew greet --configuration-cache
# 7) 想看可视化报告?
./gradlew build --scan # 上传到 gradle.com 查看
./gradlew build --profile # 离线生成 build/reports/profile/*.html期望输出片段
> Task :countLines
✅ 行数统计完成:5(已写入缓存友好型 task)
BUILD SUCCESSFUL in 1s
# 第二次运行:
> Task :countLines UP-TO-DATE
BUILD SUCCESSFUL in 0.5s
```txt [settings.gradle.kts]
/*
* 第 9 章 · 演示远程 Build Cache 的 settings 配置
*/
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
}
}
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}
// ★ 重点:声明 Build Cache(本地 + 远程)
buildCache {
local {
// 本地缓存默认就是开的,写出来更显式
isEnabled = true
// 缓存最长保留时间(默认 7 天)
// removeUnusedEntriesAfterDays = 7
}
// 取消下面注释即可启用远程缓存
// remote<HttpBuildCache> {
// url = uri("https://gradle-cache.mycompany.com/cache/")
// // 只允许 CI 写入,开发者只读,避免污染
// isPush = providers.environmentVariable("CI").orNull == "true"
// credentials {
// username = providers.gradleProperty("cacheUser").orNull ?: ""
// password = providers.gradleProperty("cachePass").orNull ?: ""
// }
// }
}
rootProject.name = "ch09-performance-demo"java
// 给 countLines 任务用的占位源文件
public class Sample {
public static void main(String[] args) {
System.out.println("Hello, performance!");
}
}build.gradle.kts ↗ · gradle.properties ↗ · README.md ↗ · settings.gradle.kts ↗ · src/main/java/Sample.java ↗