Bazel 内存优化完全指南:限制、回收与 Profiling 实战
2026/9/12 2:57:47 网站建设 项目流程

Bazel 内存优化完全指南:限制、回收与 Profiling 实战

【免费下载链接】bazela fast, scalable, multi-language and extensible build system项目地址: https://gitcode.com/GitHub_Trending/ba/bazel

导读:本文围绕 Bazel 官方文档 Optimize Memory 展开,系统讲解三种控制 Bazel 内存占用与回收的策略——通过--host_jvm_args限制 JVM 堆上限、通过--discard_analysis_cache/--nokeep_state_after_build/--notrack_incremental_state以牺牲增量构建速度换取低内存、以及实验性的 Skyfocus 机制在保留增量速度的同时聚焦工作集以回收堆内存。同时介绍内置内存分析器(Memory Profiler)与bazel dump系列命令的完整排查流程。读完本文,你将掌握应对大型仓库 OOM(OutOfMemoryError)与高内存占用的全套命令行方案,并理解每个标志背后的源码级原理。


一、概述:Bazel 的内存问题从何而来

Bazel 是一个运行在 JVM 之上的构建系统,其分析(analysis)与执行(execution)阶段会在内存中维护大量状态:Skyframe 增量依赖图、已加载的包(package)、已配置的目标(configured target)、动作缓存等。仓库越大、目标越多,这些状态所占用的堆内存就越高。当堆内存不足时,Bazel 会抛出OutOfMemoryError(OOM)导致构建失败。

官方文档 Optimize Memory 给出了三条清晰的优化路径,对应不同的取舍:

策略核心手段收益代价
限制堆上限--host_jvm_args=-Xmx…强制控制峰值内存堆不足时可能 OOM 或更频繁 GC
放弃增量状态--discard_analysis_cache--nokeep_state_after_build--notrack_incremental_state显著降低构建期间内存后续增量构建变慢,甚至需要全量重建
聚焦工作集(Skyfocus,实验性)--experimental_enable_skyfocus+--experimental_active_directories既省内存又保留增量速度工作集之外的改动会被拒绝/报错

下文逐一展开。


二、限制 Bazel 使用的内存:--host_jvm_args

2.1 基础用法

Bazel 的守护进程(server)运行在 JVM 中,其堆大小由 startup flag--host_jvm_args控制。要限制最大堆,直接传入 JVM 参数即可:

bazel build //pkg:target --host_jvm_args=-Xmx2g

上述命令将 Bazel server 的 JVM 最大堆限制为 2GB。-Xmx是标准 JVM 堆上限参数,可配合单位kmg(如-Xmx512m-Xmx4g)。

从源码看,该选项定义于 HostJvmStartupOptions.java,并在startup_options.txt(见 startup_options.txt)中有对应文档条目,属于 Bazel 启动(startup)选项,因此必须在bazel命令与子命令之间、且在解析其他命令行参数之前生效。

2.2 适用场景与注意事项

  • 适用于:CI 容器、共享开发机等内存受限环境,需要强制 Bazel 的峰值堆不超过某个阈值。
  • 注意--host_jvm_argsstartup flag,因此必须放在命令开头(bazel --host_jvm_args=-Xmx2g build …bazel build --host_jvm_args=-Xmx2g …的语义相同,Bazel 会自动识别 startup flag 并前移)。若要长期生效,推荐写入.bazelrcstartup段:
# .bazelrc startup --host_jvm_args=-Xmx2g
  • 局限:仅限制 JVM 堆并不能主动降低内存占用;当构建本身需要的内存超过上限时,仍然会触发OutOfMemoryError。因此它更适合与第三节的"放弃增量状态"策略配合使用。

三、牺牲增量构建速度换取内存

如果构建规模过大导致 OOM,可以组合使用三个命令标志,让 Bazel 在构建过程中尽可能少地保留内存状态,代价是后续增量构建速度下降:

bazel build //pkg:target \ --discard_analysis_cache \ --nokeep_state_after_build \ --notrack_incremental_state

文档明确指出:这三个标志组合使用可将单次构建的内存占用压到最低,但会使后续构建比标准增量构建更慢。这三个标志也可以单独使用,各自的效果不同。

3.1--discard_analysis_cache:分析缓存,执行期更省内存

  • 减少的是执行阶段(execution)而非分析阶段的内存。
  • 增量构建时不需要重新做包加载(package loading),但需要重做分析与执行
  • 好在磁盘上的 action cache 可以避免大部分动作的真正重执行。

从实现上看,--discard_analysis_cache的作用发生在分析完成后立即丢弃分析结果缓存,从而在执行阶段释放大量堆内存。

3.2--notrack_incremental_state:不记录依赖边

  • Bazel 内部维护一张增量依赖图(Skyframe 图),--notrack_incremental_state让 Bazel不存储任何边(edges),使该图对增量构建不可用。
  • 这些数据在下一次构建时才会被丢弃,在此之前会保留用于内部调试——除非同时指定--nokeep_state_after_build

源码佐证:该选项定义于 CommonCommandOptions.java,默认值为true;其effectTagsLOSES_INCREMENTAL_STATE(丢失增量状态),帮助信息中建议:当设为false时,通常还应该同时指定--batch。注意其旧名称是keep_incrementality_data

3.3--nokeep_state_after_build:构建结束后立即清理

  • 构建结束后丢弃全部内存状态,后续增量构建必须从头开始(磁盘 action cache 除外)。
  • 单独使用时,它不影响当前构建的峰值内存(high-water mark),只影响构建结束后的内存驻留。

源码佐证:该选项独立定义于 KeepStateAfterBuildOption.java,默认值同样为true。它与CommonCommandOptions分离是为了避免SkyframeExecutor引用CommonCommandOptions造成依赖环——这说明它直接参与 Skyframe 执行器的状态生命周期管理。

3.4 三者差异速查

标志何时释放内存增量影响磁盘 action cache 是否仍可用
--discard_analysis_cache分析完成后、执行期间重新分析+执行,免重新加载包
--notrack_incremental_state数据保留到下次构建前图不可用于增量,下次构建丢弃
--nokeep_state_after_build构建结束后下次构建全量重来

3.5 实操建议

  • CI 一次性构建:三个标志一起上,把单次构建内存压到最低。
  • 本地反复迭代:优先只加--discard_analysis_cache,兼顾内存与部分增量能力。
  • 长期配置:写入.bazelrcbuild段即可全局生效:
# .bazelrc build --discard_analysis_cache build --nokeep_state_after_build build --notrack_incremental_state

四、Skyfocus:保留增量速度的内存回收方案(实验性)

第三节的方案以牺牲增量速度换取内存。如果希望既降低内存、又保留增量构建速度,Bazel 提供了实验性功能Skyfocus:你告诉 Bazel 将要修改的"工作集"(working set)文件,Bazel 就只为正确增量重建这些文件的改动而保留必要状态,其余 Skyframe 状态全部回收。

4.1 启用方式与默认工作集

bazel build //pkg:target --experimental_enable_skyfocus
  • 默认情况下,工作集 =被构建目标旁边的一组文件。上例中,//pkg下的所有文件都会保留在工作集内。
  • 工作集之外的任何文件改动都会被禁止(构建报错),直到你执行bazel clean或重启 Bazel server。

4.2 指定精确工作集:--experimental_active_directories

如果默认工作集不符合需求,可用--experimental_active_directories指定精确的文件/目录集合:

bazel build //pkg:target --experimental_enable_skyfocus \ --experimental_active_directories=path/to/another/dir,path/to/tests/dir

注意该标志接收以逗号分隔、相对于工作区根目录(workspace root)的路径列表

源码佐证:这些选项全部定义于 SkyfocusOptions.java:

  • experimental_enable_skyfocus(默认false):总开关,帮助文本明确指出"启用后通过--experimental_active_directories降低增量构建的内存占用,该功能即 Skyfocus";
  • experimental_active_directories(默认""):有状态标志(stateful flag)——一旦定义,会在后续调用中持续生效,直到重新定义新集合。这点对日常使用很重要:指定一次后,后续构建会沿用该工作集;
  • experimental_skyfocus_dump_keys(默认none):调试用,可取值none/count/verbose,分别用于输出被聚焦的 SkyKeys(roots、leafs、focused deps、focused rdeps)的数量统计或字符串表示;
  • experimental_skyfocus_dump_post_gc_stats(默认false):调试用,启用后在聚焦前后手动触发 GC,以报告堆内存缩减量(会增加 Skyfocus 延迟)。

此外还有两个边界策略选项:

  • experimental_frontier_violation_check(默认strict):处理"工作集之外发生改动"的策略,可取strict(显式报错,要求用户处理)、warn(尝试优雅处理并输出警告)、disabled_for_testing(仅测试用);
  • experimental_frontier_violation_verbose(默认false):为true时打印修复 Skycache 违规的指引。

Skyfocus 的执行逻辑位于 SkyfocusExecutor.java,由 SkyframeExecutor.java 调用,是整个 Skyframe 状态管理的一部分。

4.3 完整示例与内存收益度量

组合使用上述标志并加上--experimental_skyfocus_dump_post_gc_stats显示内存回收量,官方文档给出的完整输出如下:

$ bazel test //pkg:target //tests/... --experimental_enable_skyfocus --experimental_active_directories=dir1,dir2,dir3/subdir --experimental_skyfocus_dump_post_gc_stats INFO: --experimental_enable_skyfocus is enabled. Blaze will reclaim memory not needed to build the working set. Run 'blaze dump --skyframe=working_set' to show the working set, after this command. WARNING: Changes outside of the working set will cause a build error. INFO: Analyzed 149 targets (4533 packages loaded, 169438 targets configured). INFO: Found 25 targets and 124 test targets... INFO: Updated working set successfully. INFO: Focusing on 334 roots, 3 leafs... (use --experimental_skyfocus_dump_keys to show them) INFO: Heap: 1237MB -> 676MB (-45.31%) INFO: Elapsed time: 192.670s ... INFO: Build completed successfully, 62303 total actions

分析这个示例:

  • 本次构建分析了 149 个目标、加载了 4533 个包、配置了 169438 个目标;
  • 工作集更新成功,聚焦了 334 个 roots 和 3 个 leafs(可用--experimental_skyfocus_dump_keys查看具体键);
  • 堆内存从 1237MB 降至 676MB,回收 561MB(约 45.31%)
  • 后续对dir1dir2dir3/subdir下文件的增量构建保持原有速度;
  • 代价是:这些目录之外的文件一旦改动,Bazel 无法增量重建(默认strict模式下直接报错)。

4.4 查看工作集

文档给出的示例输出中提示:构建后可运行bazel dump --skyframe=working_set(Bazel 中对应blaze dump --skyframe=working_set)来查看当前工作集内容,便于确认 Bazel 实际聚焦了哪些路径。

4.5 使用要点总结

  1. Skyfocus 是实验性功能,行为可能随版本变化,以当前仓库实现为准;
  2. 工作集之外的改动在strict默认策略下会直接导致构建错误,需要bazel clean或重启 server 才能解除聚焦;
  3. --experimental_active_directories有状态标志,设置后持续生效,重新设置才会覆盖;
  4. 生产使用前建议先在分支/CI 上验证工作集划分是否合理,避免频繁触碰工作集外文件。

五、内存分析(Memory Profiling):定位规则级内存热点

如果只是想限制或回收内存还不够,你还需要知道内存到底被谁吃掉了。Bazel 内置内存分析器专门用于检查自定义规则的 Starlark 内存占用。本节完整整理官方"自定义规则性能"文档 Memory profiling 中的流程。

5.1 启用内存跟踪(必须的两个 startup flag)

要让 server 进入内存跟踪模式,每次Bazel 调用都必须携带以下两个 startup flags:

STARTUP_FLAGS=\ --host_jvm_args=-javaagent:<path to java-allocation-instrumenter-3.3.4.jar> \ --host_jvm_args=-DRULE_MEMORY_TRACKER=1

其中java-allocation-instrumenter-3.3.4.jar需要从 Maven Central 下载(坐标为com.google.code.java-allocation-instrumenter:java-allocation-instrumenter:3.3.4),并替换<path to …>为实际路径。

⚠️ 关键约束:只要有一次 Bazel 调用漏掉这两个 flag,server 就会重启并丢失跟踪状态,必须重新开始。因此实践中应把STARTUP_FLAGS固化到.bazelrcstartup段或脚本中。

从源码看,RULE_MEMORY_TRACKER系统属性与内存跟踪模块相关联,相关实现位于 AllocationTrackerModule.java,即src/main/java/com/google/devtools/build/lib/profiler/memory/包内,负责分配追踪与堆转储支撑。

5.2 第一步:只分析不构建

以目标foo为例,只运行分析阶段、跳过执行阶段,加--nobuild

$ bazel $(STARTUP_FLAGS) build --nobuild //foo:foo

5.3 第二步:查看整个 Bazel 实例的内存占用

$ bazel $(STARTUP_FLAGS) info used-heap-size-after-gc > 2594MB

info used-heap-size-after-gc报告 GC 后的堆使用量,是衡量 Bazel 进程整体驻留内存的快捷方式。

5.4 第三步:按规则类型分解内存

$ bazel $(STARTUP_FLAGS) dump --rules > RULE COUNT ACTIONS BYTES EACH genrule 33,762 33,801 291,538,824 8,635 config_setting 25,374 0 24,897,336 981 filegroup 25,369 25,369 97,496,272 3,843 cc_library 5,372 73,235 182,214,456 33,919 proto_library 4,140 110,409 186,776,864 45,115 android_library 2,621 36,921 218,504,848 83,366 java_library 2,371 12,459 38,841,000 16,381 _gen_source 719 2,157 9,195,312 12,789 _check_proto_library_deps 719 668 1,835,288 2,552 ... (more output)

bazel dump --rules按规则类(rule class)输出:实例数(COUNT)、动作数(ACTIONS)、总字节数(BYTES)与单实例平均字节(EACH)。上例中android_library单实例高达 83KB、proto_library单实例 45KB,都是值得优先排查的对象。

5.5 第四步:导出 Starlark 堆转储并用 pprof 分析

$ bazel $(STARTUP_FLAGS) dump --skylark_memory=$HOME/prof.gz > Dumping Starlark heap to: /usr/local/google/home/$USER/prof.gz

dump --skylark_memory=<file>导出 Starlark 堆的 pprof 格式文件。随后用 pprof 分析:

生成火焰图:

$ pprof -flame $HOME/prof.gz

生成带行号的文本热点:

$ pprof -text -lines $HOME/prof.gz > flat flat% sum% cum cum% 146.11MB 19.64% 19.64% 146.11MB 19.64% android_library <native>:-1 113.02MB 15.19% 34.83% 113.02MB 15.19% genrule <native>:-1 74.11MB 9.96% 44.80% 74.11MB 9.96% glob <native>:-1 55.98MB 7.53% 52.32% 55.98MB 7.53% filegroup <native>:-1 53.44MB 7.18% 59.51% 53.44MB 7.18% sh_test <native>:-1 26.55MB 3.57% 63.07% 26.55MB 3.57% _generate_foo_files /foo/tc/tc.bzl:491 26.01MB 3.50% 66.57% 26.01MB 3.50% _build_foo_impl /foo/build_test.bzl:78 22.01MB 2.96% 69.53% 22.01MB 2.96% _build_foo_impl /foo/build_test.bzl:73 ... (more output)

-lines会把热点精确到.bzl文件的具体行号(如_build_foo_impl /foo/build_test.bzl:78),这正是定位规则内存泄漏/过度分配的最直接证据。

说明:pprof是独立的 Go 工具,需另行安装(例如通过go install或系统包管理器获取 google/pprof),不随 Bazel 分发。

5.6 内存排查完整流程小结

步骤命令目的
0两个 startup flags(见 5.1)开启内存跟踪模式
1bazel build --nobuild //foo:foo只分析不执行
2bazel info used-heap-size-after-gc看整体堆占用
3bazel dump --rules按规则类型分解
4bazel dump --skylark_memory=prof.gz导出 pprof 堆转储
5pprof -flame/pprof -text -lines定位热点行

六、综合决策建议

场景推荐方案
CI 单次构建、内存紧张--host_jvm_args=-Xmx…+ 第三节三标志组合
本地大仓增量迭代、内存吃紧--discard_analysis_cache单独使用
需要同时保内存与增量速度尝试 Skyfocus +--experimental_active_directories
规则内存异常、需要定位5.1 的两个 startup flags +bazel dump系列

所有标志的最终权威定义均可在源码中核对:CommonCommandOptions.java、KeepStateAfterBuildOption.java、SkyfocusOptions.java 与 HostJvmStartupOptions.java;规则内存分析流程的完整版见 自定义规则性能文档。

【免费下载链接】bazela fast, scalable, multi-language and extensible build system项目地址: https://gitcode.com/GitHub_Trending/ba/bazel

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询