IDE 或 CLI 内存暴涨到卡死:四个吃内存的地方怎么逐个排掉
数据截至 2026-07,各产品的额度与报错口径以官方最新说明为准。
**内存暴涨到卡死,最常被归错的因是”机器不行”,而实际上绝大多数情况下吃掉内存的是你自己喂进去的东西:一次失控的索引、一段没人收尾的长对话、几个当时顺手拖进去的大附件、以及一堆你以为已经结束的后台任务。**换机器能让同一个问题晚半小时出现,但不会让它消失。所以第一步不是加硬件,而是把内存归因到这四个来源之一——这一步做对了,处置动作往往只需要一两分钟。
站内有两篇邻居各管一段:索引本身报错、进度条走不完是另一类问题,看 Cursor 代码库索引卡住、失败怎么解决;如何组织对话让上下文不失控是方法论,看 Claude Code 上下文管理。这篇只管一件事——当进程的内存占用已经在往上爬、机器开始卡的时候,怎么按成本从低到高的顺序把它归因并压下去。顺带说明:Claude Code 这类海外工具,官方对中国大陆有区域限制、不支持直连,市面上存在第三方中转但本文不背书、也不给具体渠道;下面讲的内存机制与你走什么链路无关。
一、先量一次,别猜
猜的成本很低,猜错的成本很高——你会花二十分钟清一个根本不占内存的缓存。所以先花一分钟拿到三个数:哪个进程在涨、涨得多快、涨的时候你在干什么。
现代编辑器不是一个进程,是一窝。主进程、渲染进程、语言服务、文件监听、插件宿主、以及 AI 功能自己的子进程,各自独立占内存。只看任务管理器里那个总数,你分不清是谁的锅。
在类 Unix 环境下,按占用排一遍常驻内存(RSS)。下面这条用管道排序,Linux 与 macOS 都能跑:
# 按 RSS 倒序列出前 15 个进程,RSS 单位 KB
ps -eo pid,ppid,rss,comm | sort -k3 -nr | head -15
--sort=-rss 这类写法只有 Linux 的 procps 支持,macOS 自带的是 BSD 版 ps,会直接报 illegal option;Windows 下走 WSL 或者用任务管理器的「详细信息」标签按内存列排序,同样能定位到进程。
盯住排头那个的 pid,隔一段时间再看一次同一个 pid:
# 每 5 秒打一次某个进程的 RSS,观察是稳住还是持续爬升
while true; do ps -o rss= -p <pid>; sleep 5; done
这里要分清两种曲线,处置方向完全不同:
- 台阶型:你做了某个动作(打开大文件、粘贴一大段日志、发起一次全库检索),内存跳一级,然后稳住。这是正常的按需加载,问题在你喂的量,不在程序。
- 爬坡型:你什么都没做,它还在慢慢涨,涨了不回落。这是持有没释放,可能是长对话累积、可能是后台任务没退出、也可能是插件泄漏。
再补一句常被忽略的:如果机器已经开始换页(swap 在动),你观察到的”卡”里有一大半是磁盘等待,不是 CPU 忙。此时任何操作都会变慢,包括你的排查动作本身。先把最占的那个进程停掉再排查,否则你是在泥里跑。
二、判别表:现象 → 成因 → 验证 → 处置
| 现象 | 大概率成因 | 怎么验证 | 处置动作 |
|---|---|---|---|
| 刚打开项目或刚切完分支,几分钟内内存冲到高位,CPU 也满 | 索引在全量重建,且扫描范围里混进了不该扫的目录 | 看进程名是不是语言服务/索引相关;用下面的脚本列仓库里的大文件与大目录 | 把构建产物、依赖目录、数据集、二进制资源加进忽略规则,然后手动触发一次重建 |
| 同一个会话聊了很久,越往后越卡,新开一个会话立刻恢复 | 长对话累积,历史消息与检索结果都还被持有 | 新开窗口做同样一件事,对比 RSS;这是最干净的对照实验 | 收尾当前会话,把结论落成文件,新会话只带这份结论 |
| 拖进图片、日志、数据文件之后立刻掉一个台阶 | 附件被完整读入并可能被多次转换保存 | 新开一个会话,同一件事只贴裁剪后的片段,对比两次的 RSS 台阶高度(别指望在原会话里撤回附件后 RSS 回落,内存管理器通常不会立刻把已分配的页还给系统) | 附件只留必要片段;大日志先在命令行里裁剪,只贴关键行 |
| 关掉对话面板内存也不回落,风扇继续转 | 后台任务还活着:终端里没退出的进程、监听式构建、被 Agent 拉起的子进程 | 列出该进程的所有子进程,看有没有你不认识的 | 先定位再终止;能收敛的收敛,不能收敛的记下来单独跑 |
| 用一段时间必卡,重启后必好,与项目大小无关 | 某个插件或扩展在泄漏 | 禁用一半插件跑同样的流程,二分法收敛 | 找到后禁用或替换;替换不了就接受定时重启 |
| 只有跑某一类任务时爆,其他时候正常 | 单次喂入的量超过机器承受,属于容量问题不是缺陷 | 把同一任务的输入砍掉一半再跑 | 拆任务、分批做;见第四节的止损判断 |
列大文件与大目录,用这段通用脚本(在仓库根目录执行):
import os
SKIP = {'.git', 'node_modules', 'dist', 'build', 'target', '.venv', '__pycache__'}
rows = []
for root, dirs, files in os.walk('.'):
dirs[:] = [d for d in dirs if d not in SKIP]
for name in files:
path = os.path.join(root, name)
try:
size = os.path.getsize(path)
except OSError:
continue
if size > 1024 * 1024:
rows.append((size, path))
for size, path in sorted(rows, reverse=True)[:20]:
print(f'{size // 1024} KB\t{path}')
注意脚本里 SKIP 的作用是让你看清”应该被索引的那部分有多大”。跑完这一遍,把 SKIP 改成 set() 再跑第二遍,对比两次的结果:如果第二遍冒出一堆你从没打开过的大文件,说明真正的问题是索引范围没收住,而不是你的源码太大。这两个数的差距,就是你能靠改忽略规则省下来的那部分。
三、四个来源各自的处置动作
索引:先收范围,再谈重建
索引吃内存的量级由被扫描的文件数与总字节数决定,跟你自己写了多少行代码关系不大。真正把它撑爆的通常是三类东西:依赖目录、构建产物、以及被误提交的数据文件和二进制资源。
动作有先后:先把忽略规则改对,再重建一次;顺序反了就是白建一遍。 忽略规则改完之后,值得顺手确认一下工作区是不是干净——切分支后残留的大量未跟踪文件同样会被扫描:
git status --porcelain | wc -l
这个数如果异常大,先弄清那些文件是什么再重建。另外一个高频触发点是频繁切分支。切换时 git 只重写两个分支之间有差异的那些文件,但这些文件的修改时间会被更新——差异越大、重写的文件越多,文件监听和索引就会跟着大面积失效重算。跨版本跳跃、切到很久没同步的分支,都属于这一类。要并行做多件事,用独立工作目录比反复切分支便宜得多,见 Claude Code worktree 并行。
长对话:把状态搬出内存
一次对话越往后,需要被持有的东西越多:历史消息、检索命中的代码片段、工具调用的输入输出。各家在什么时候压缩、丢弃或转存,规则不同且会调整,以官方最新说明为准。你能控制的是自己这一侧:别让一个会话既做调研、又做重构、又做调试。
可执行的做法是给会话设收尾点。一个阶段做完就把结论写进文件——决策、改动清单、还没验证的疑点——然后新开会话,只带这份文件。这样做除了省内存,还顺带解决了”聊到后面它忘了前面”的问题。具体怎么组织,开头那篇上下文管理里讲得更细,本文不重复。
反过来说,如果你的仓库大到常规检索总是给不出足够上下文,那是另一个方向的问题,别用”多聊几轮、多贴几段”去硬凑——那条路的终点正是这篇要治的内存曲线。
附件:在命令行里先裁一刀
拖进去的文件在内存里通常不止一份:原始字节、解码后的表示、可能还有编码后用于传输的副本。图片和长日志是两个最容易翻车的类型。
日志的正确做法是先裁再贴。比如只要出错前后各若干行:
# 找到第一次出现 ERROR 的位置,连同上下文一起取出
grep -n -m1 -B 20 -A 60 'ERROR' app.log > /tmp/slice.log
或者只要尾部:
tail -n 200 app.log > /tmp/slice.log
判断标准很朴素:你自己都不会逐行读的内容,喂进去也不会变成有用的判断,只会变成内存和成本。 大截图同理,裁到只剩报错区域,识别效果往往还更好。
后台任务:你以为结束了的那些
这是最容易被漏掉的一类,因为界面上看不见。常见的有:Agent 在终端里拉起的进程没退出、监听式构建一直在跑、测试守护进程还活着、上一次被你中断的任务其实只断了前台。
先看清父子关系:
# 列出某个进程的直接子进程(Linux / WSL)
ps -o pid,rss,etime,args --ppid <pid>
# macOS 上 --ppid 不可用,先取子进程号再查详情
ps -p "$(pgrep -P <pid> | paste -sd, -)" -o pid,rss,etime,args
etime 那一列很有用——一个已存活很久、你却记不起是什么时候启动的进程,基本就是漏收的。终止之前先看清 args,别把语言服务当垃圾清了。
后台任务安静地失败或安静地活着,是一类独立的问题,处置思路在 后台 Agent 静默失败 里有专门讨论。
对于你自己启动的 Node 类命令行工具,可以给它一个明确的堆上限,让它在吃光机器之前先自己失败,而不是把整台机器拖进换页:
# 单位 MB,值按你机器能承受的范围给
NODE_OPTIONS=--max-old-space-size=4096 your-cli-command
两个前提别搞错:一是这个参数管的是 V8 老生代堆的上限,Buffer、原生扩展占的那部分不在里面,所以进程实际 RSS 会比这个数高一截,别把它当进程内存总闸;二是它只对 Node 实现的命令行工具生效,用 Rust、Go 编译出来的工具设了也没有任何作用——不确定就先看一眼这个工具是怎么装的(npm i -g 装进来的基本是 Node)。想知道当前默认上限是多少,跑 node -e "console.log(require('v8').getHeapStatistics().heap_size_limit / 1048576)",它会按你机器的实际内存给出一个值,不同机器不一样,所以别照抄别人贴的数字。
这条不是优化,是保险:一个及时失败的进程比一台卡死的机器好收拾。
四、什么情况下别再折腾了
排查内存问题有个陷阱:它每一步都看起来”再试一下就好了”。给自己设几条硬线。
止损点一:同一个方向连试三次没有改善,就换方向。 你清了三遍缓存内存还是爬坡,说明成因不在缓存。继续清是沉没成本。
止损点二:机器已经在换页,先停手。 此时你的每个操作都要等磁盘,观察到的现象全部被污染,判断可靠性极低。正确动作是终止最占的进程、恢复响应,再从第一节重新量一次。
止损点三:重启能解决且泄漏源在你控制之外,就接受重启。 如果二分法定位到某个第三方插件在泄漏,而你既不能改它也没有替代品,那么”每天重启一次”是合理工程决策,不是失败。把它写进你的日常操作里,比每天现场排查一遍便宜。
止损点四:需要正确性的改动,别在卡死边缘继续做。 内存吃紧的时候,任务被截断、写文件写一半、Agent 拿到不完整的结果继续往下推,都会发生。这类改动出来的代码可能表面能跑但语义错了。此时该做的是回滚到上一个干净提交,恢复环境,再重来——而不是在废墟上继续盖。
# 先确认要丢的是什么,再决定怎么丢
git status
git diff --stat
要保留半成品就 git stash;确认可以丢弃再用重置类命令。任何会丢工作的命令,执行前先让 git status 告诉你要丢什么。 关于 AI 改坏代码之后怎么回滚得干净,另有一篇专门写了,见 AI 改坏代码怎么回滚。
换条路的判断依据:如果一个任务在你的机器上无论怎么调都吃不下,那它不是内存问题,是任务粒度问题。把它拆成几个各自独立可验证的小任务,是唯一稳定的解法。指望调参数把一个超出机器容量的任务塞进去,不成立。
五、避坑清单
坑一:一上来就加内存或换机器。 为什么会踩——症状太像硬件不够,而加内存是唯一不需要理解成因的动作。怎么避——先按第一节量出是台阶型还是爬坡型;爬坡型是持有没释放,加内存只是延后爆炸时间。
坑二:把依赖目录和构建产物留在索引范围里。 为什么会踩——它们不在你的注意力里,你写代码时从不打开它们,很容易忘了索引会。怎么避——新项目接入 AI 工具的第一件事就是核对忽略规则,用第二节的脚本看一眼实际体量。
坑三:一个会话从早聊到晚。 为什么会踩——上下文连续确实让协作更顺,中断会话有心理成本。怎么避——给会话设明确的收尾点,把结论落成文件再新开;文件比对话历史更可靠,也更便宜。
坑四:顺手拖大文件进去。 为什么会踩——拖拽只要一秒,代价却发生在看不见的地方。怎么避——养成”先裁再贴”的习惯,日志用 grep/tail 取片段,截图裁到报错区域。
坑五:只关面板不看进程。 为什么会踩——界面收起来了,人就默认任务结束了。怎么避——遇到内存不回落,一律用 ps --ppid 看一眼子进程,别信界面。
坑六:一次性禁用全部插件来”确认”是不是插件问题。 为什么会踩——它见效快,能立刻验证方向。怎么避——全禁只能告诉你”是插件”,二分法才能告诉你”是哪个”;多花两轮,换来可以长期使用的结论。
坑七:把内存问题和模型质量问题混在一起改。 为什么会踩——两者的表现有重叠,内存吃紧时输出常常被截断,看起来就像模型变差了。怎么避——先把环境稳住再评估输出质量,顺序反了你会拿被污染的样本去下结论。
坑八:不记录基线。 为什么会踩——没人愿意在一切正常的时候做记录。怎么避——在机器状态良好时随手存一次进程 RSS 排名。有基线,下次异常你三十秒就能判断”这个数是不是真的高”;没基线,你只能靠感觉。
收束
内存暴涨这件事,难的不是修,是归因。四个来源里,索引和附件是你喂进去的量,长对话和后台任务是没释放的持有;量的问题靠收范围、裁片段解决,持有的问题靠收尾、终止解决。方向对了,动作都很短。
留一份自检清单,卡的时候从上往下走:
- 按 RSS 排一次进程,锁定是哪个 pid 在涨。
- 隔几次采样看曲线:台阶型还是爬坡型。
- 台阶型 → 回想最近三个动作,撤回可疑附件或缩小检索范围。
- 爬坡型 → 先查子进程有没有漏收的,再查会话是不是聊太久,最后用二分法查插件。
- 已经换页 → 先终止最占的进程恢复响应,再重新量。
- 改动可能被截断 → 用
git status/git diff --stat看清现场,该回滚就回滚。 - 调不下去 → 拆任务,别调参数硬塞。