打开网易新闻 查看精彩图片

GitHub Actions用户去年在issue区吵了47页。一台标注32GB内存的runner,给Gradle堆开到16GB,构建照样被系统杀死。问题不在JVM,在所有人都在看错的指标。

内存(Heap)只是JVM向操作系统借的一块地。操作系统眼里的Java进程,是个更庞大的存在——元空间(Metaspace)、线程栈、直接内存、JNI开销、GC保留区,全部算进RSS(Resident Set Size,常驻内存集)。16GB堆的进程,RSS轻松飙到22GB以上,这还没算Kotlin守护进程、测试JVM、Android的Lint和R8。

更麻烦的是死亡方式。OOM时宿主机直接发SIGKILL,Gradle没机会留遗言。CI环境又没法挂事后钩子抓现场,用户只能对着"Process completed with exit code 137"发呆——137就是Linux的OOM杀手签名。

作者为此写了个GitHub Action叫Process Watcher。思路很朴素:既然死后验尸来不及,那就派个探子活着的时候盯梢。

RSS才是真正的内存血压计

RSS才是真正的内存血压计

Unix系统里,RSS反映进程实际占用的物理内存。查法也简单:

ps -o rss= -p "$PID"

打开网易新闻 查看精彩图片

输出是KB,比如654321就是约639MB。但这串数字有个致命缺陷:它是快照,不是录像。构建被杀那一刻,你手里只有一张遗照。

Process Watcher的做法是起个独立监控进程,每5秒扫一圈所有JVM进程的RSS、堆用量、GC活动。数据实时落盘,构建完归档成CSV。典型长这样:

Elapsed_Time | PID | Name | Heap_Used_MB | Heap_Capacity_MB | RSS_MB
00:00:05 | 149 | GradleDaemon | 29.7 | 86.0 | 241.0
00:00:10 | 149 | GradleDaemon | 191.7 | 338.0 | 560.1

三列数字摆在一起,很多"灵异现象"瞬间现形。堆只用了200MB,RSS却冲到500MB——说明直接内存或线程在疯长。堆和RSS同步暴涨——可能是大对象分配或GC跟不上。堆回收了RSS没掉——内存碎片或元空间泄漏。

多进程才是内存黑洞的真身

多进程才是内存黑洞的真身

Gradle构建从来不是独角戏。Kotlin编译器有个守护进程,测试任务起独立JVM,Android构建链里Lint查代码、R8做混淆,全是吃内存的主。

Process Watcher的监控范围刻意做宽:不只盯Gradle Daemon,所有带"java"的进程全入库。实际跑下来,有些构建的内存峰值出现在测试阶段——四个测试JVM并行,每个8GB堆,RSS叠加后直接把机器顶穿。

打开网易新闻 查看精彩图片

GitHub Actions的runner规格是透明的。标准Linux runner标称16GB内存,但可用量约14GB出头。扣掉系统开销,留给构建的硬天花板其实很低。很多人按"堆=内存/2"的经验公式配置,没算RSS的膨胀系数,也没算多进程的叠加效应。

CSV数据拉进Excel画条曲线,问题一目了然。某次构建的RSS曲线在12分钟处垂直上冲,对应测试任务启动四个并行JVM。曲线触顶后3秒,进程消失——OOM杀手出手。没有监控的话,这3秒的黑箱足够让人猜三天。

从静态配置到动态观测

从静态配置到动态观测

JVM调优的老思路是算公式、配参数、祈祷。XX:+UseG1GC、-Xmx、-XX:MaxMetaspaceSize,调完跑一遍,挂了再调。RSS监控把这套改成数据驱动:先看实际压力曲线,再决定哪里动刀。

Process Watcher的输出生成两种视图。时间序列看趋势,哪一阶段内存陡增;进程维度看分布,哪个JVM是罪魁祸首。有些项目发现Kotlin Daemon的RSS长期比堆高40%,果断关掉守护进程模式,每次构建冷启动反而更稳。

GitHub Actions的市场里,类似工具不少。但多数只报最终状态,或者只监控单个进程。Process Watcher的差异化在于"全进程+时间序列+CI原生"——Action市场里搜"memory monitoring",它是少数能画出RSS曲线的。

作者的原话很直白:「OOM场景恰恰是能见度最重要的时候,但我们通常什么都看不到。」Process Watcher补的就是这个缺口。构建被杀前,数据已经落地;构建失败后,CSV随artifact上传,本地用Excel或Python复盘。

有个细节很有意思。早期版本只采RSS和堆,用户反馈说GC频率也很关键——Full GC触发时RSS会假性下跌,因为内存还给系统了,但压力没减。后来加了GC计数列,曲线解读更准确。

现在的问题是:你的CI构建,上一次看到完整的内存压力曲线是什么时候?