Skip to content

第 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 help6-8 秒0.5-1 秒
./gradlew clean build90 秒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 Cachesettings.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(compileJavatestjarcompileKotlin 等)开箱即用
  • 自定义 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.5s1.5s0.3s
./gradlew test(增量)8s4s0.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 + parallel3:40-1:32
2开 build cache(本地)2:15-1:25
3自定义 task 都补 inputs/outputs1: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 条标准:

  1. 输入/输出都能精确声明(不能依赖外部不可控因素)
  2. 同输入永远能产出相同输出(确定性)
  3. 输出文件较大、计算耗时较长(缓存才划算)

满足全部 → 加 @CacheableTask

Q6:Worker API 比直接开线程好在哪?

  • 自动接入 Gradle 的并行调度(max-workers
  • 支持 ProcessIsolation(进程隔离),用得安全
  • 异常会聚合到 Gradle 报告里,定位方便
  • 进度信息会显示在终端 / Build Scan 上

Q7:构建慢,用什么命令排查?

  1. --scan 上传到 gradle.com 看可视化报告
  2. --profile 生成本地 HTML 报告
  3. --info 看 task 决策原因
  4. 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=true
markdown
# 第 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 ↗