Ubuntu用着用着突然卡死,鼠标像陷入泥潭,切个窗口都要等好几秒,最后只能长按电源键强行断电。这个场景我太熟了——最早我以为是内存条挂了,换内存、换主板、换硬盘都试过,问题依旧。后来才想明白:绝大多数情况下,Ubuntu默认的内存管理策略在桌面场景里并不算合理,缺的不是内存本身,而是“该回收回收、该交换交换”这一整套机制的调教。
这篇文章我会从内存管理的底层逻辑讲起,再一步步给出可复制、可验证的排查命令和优化参数,最后把zram、zswap、systemd-oomd这些能真正提升体验的方案按适用场景拆开讲清楚。不管你是被Ubuntu卡死折磨的桌面用户,还是负责一堆Linux服务器的运维同学,这篇都值得花十分钟读完并备份到自己常用的运维笔记里。
1. Ubuntu卡死不是玄学:内存机制里藏着真正的元凶
1.1 空闲内存被“借走”是特性,但没人还回来就是灾难
很多人第一次用Ubuntu时都会打开系统监视器,然后发现内存占用常年接近90%,哪怕你只开了一个浏览器。这其实是Linux的一个经典设计:内核会把空闲内存尽可能拿去做文件缓存、目录缓存和缓冲,用来加速磁盘读改写。这部分内存在free命令里被标记为buff/cache,逻辑上属于“可以回收”而非“已占用”。
问题就出在这个“可以回收”上。桌面负载和服务器负载完全不同:你开着浏览器、文档工具、代码编辑器,来回切换时会不断产生新的文件缓存和匿名内存页。当物理内存接近耗尽,内核开始回收缓存时,如果回收速度跟不上分配速度,系统就会出现肉眼可见的卡顿。更麻烦的是,很多桌面用户根本没有配置可用的交换空间,或者把/swapfile建得特别小,于是内核回收无路可退,只能走直接回收路径,这时候整个系统的停顿会非常剧烈。
我用一个厨房的类比来解释为什么这会“卡死”:Linux把空闲内存当作台面上提前切好的菜(缓存),做菜时不够地方可以随时把菜收进冰箱。问题是这个冰箱(swap)如果是空的,或者小得可怜,那菜就没地方收,厨师只能边切边做,手忙脚乱。这时候你喊破喉咙,系统也只能优先保住内核自己,剩下的进程一起遭殃。
1.2 内存不足时的连锁反应:从卡顿到OOM
当物理内存和交换空间全部吃紧,内核会启动oom_killer去杀掉一些进程来释放内存。这个杀手并不智能,它主要根据进程的内存占用和oom_score来选“牺牲品”,你正在写代码的IDE、没保存的文档,都有可能被它选中。更让人恼火的是,等到oom_killer动手时,整个系统通常已经进入半死状态,你连看一眼是谁被杀的机会都没有。
而且还有一个隐藏的恶性循环:如果交换空间位于机械硬盘,或者SSD已经接近装满,内核把内存页换出的速度极慢,而进程还在疯狂申请内存,这时候你会看到整个系统几乎“冻结”,硬盘灯狂闪,鼠标动不了,按键盘没反应——其实内核还在后台拼命干活,只是它的力气全花在换页上了。
所以,Ubuntu内存优化这件事,核心不是“让我多用点内存”,而是“让内存管理系统在压力到来时有合理的路径可走”。理解了这一点,接下来所有的排查和调参才有意义。
2. 先搞清楚谁在吃内存:一套可复制的排查链路
2.1 别只会看free -h,这几个数字要知道怎么读
优化的第一步永远是量化现状。打开终端,跑free -h,很多人只知道看used这一列,其实最关键的是available。看下面这个示例输出(只是参考,实际数值因机器而异):
total used free shared buff/cache available Mem: 31Gi 4.1Gi 6.8Gi 412Mi 20Gi 25Gi Swap: 2.0Gi 0B 2.0Gi这里available才是系统实际“还能给新程序用”的内存,free只是完全没被占用的物理页,buff/cache里的很大一部分能随时回收。如果你的available经常被压到只有几百MB,甚至趋近于零,而buff/cache却居高不下,恭喜你,这正是优化空间最大的情况。
MemAvailable这个数值在内核里是估算出来的,它会考虑剩余空闲页、可回收的缓存以及swap剩余量。日常判断系统是不是“要出事”,看available比看free准得多。
2.2 用vmstat和ps判断系统是不是在疯狂换页
free只能看到快照,想看系统是不是“正在狂换页”,用vmstat 1持续刷几秒:
vmstat 1重点关注最后一列si和so,它们分别表示每秒从swap换入和换出的内存量。如果这两个数值长期很高(比如几百MB甚至上GB),说明系统一直在倒腾内存页,这种状态下干什么都卡。如果si和so一直为零,但内存和cache也吃满了,说明还没有真正触发交换,接下来就要看谁的匿名页在推高水位。
要揪出谁在吃内存,一条命令足够:
ps aux --sort=-%mem | head -20%mem列会按物理内存占用的百分比从高到低排序。需要注意的是,这个百分比是“驻留内存RSS”的近似,不是进程申请过的虚拟内存。你可能会发现Chromium系的进程一开就是几十个,每个都占几百MB,加起来非常可观。
2.3 翻日志找OOM记录:真相往往藏在journalctl里
如果系统已经发生过卡死甚至自动杀掉进程,先去日志里找证据:
journalctl -k | grep -i -E "oom|out of memory|killed process"这条命令会从内核日志里筛出所有和OOM相关的行。你会看到类似“Killed process 1234 (chrome) total-vm:...”的记录。日志里还会给出当时的内存状态摘要,包括每个进程的占用排名。这是排查“上次到底发生了什么”的最佳途径。
我有一个习惯:在完成任何内存调优之前,先把这个排查链路完整跑一遍,记录free -h、vmstat的典型值,再翻一遍OOM日志。没有这些基线数据,你后面调了参数也很难判断是变好还是变坏。表格式总结如下:
| 排查步骤 | 命令 | 核心关注点 |
|---|---|---|
| 查可用内存 | free -h | available是否长期偏低 |
| 看实时换页 | vmstat 1 | si/so是否持续较大 |
| 找大户进程 | ps aux --sort=-%mem | 谁的总内存占用最高 |
| 确认实际驻留 | smem | 比RSS更真实的物理内存占用 |
| 翻OOM历史 | `journalctl -k | grep -i oom` |
第三步的smem可能很多人没装过,它是基于/proc的统计工具,能给出USS(唯一驻留内存)、PSS(按共享比例折算的驻留内存)和RSS三列。PSS比RSS更能反映真实物理内存占用,因为它把共享库的占用按比例分摊了。抓“内存大户”时我一般先用smem -t -k -s pss。
3. 最小改动先救急:sysctl参数调优的正确姿势
3.1swappiness:内核多愿意用swap的“性格参数”
我知道很多人一上来就问“swappiness设多少”,我建议先理解它再动手。vm.swappiness的范围是0到100,默认60,表示内核在回收内存时,倾向于从swap(匿名页)回收的权重。数值越低,越不愿意用swap,优先回收文件缓存;数值越高,越积极地把不常用的匿名页换到swap里,给文件缓存留更多空间。
桌面场景下,大量内存是被文件缓存和匿名内存混合占用的。如果你用了较快的SSD,设成10可以让系统尽量别把进程的内存页换出去,卡顿感明显下降。如果你的内存特别小(比如8GB以下),完全不开swap也不行,我更推荐配合第4章的zram一起使用,让swap变成“压缩内存”而不是磁盘。
修改方式有两种:临时生效用sysctl -w vm.swappiness=10,永久生效写进/etc/sysctl.d/99-memory-tuning.conf:
echo "vm.swappiness=10" | sudo tee /etc/sysctl.d/99-memory-tuning.conf sudo sysctl -p /etc/sysctl.d/99-memory-tuning.conf3.2vfs_cache_pressure:回收目录和文件元数据缓存的力度
这个参数控制内核回收目录项(dentry)和索引节点(inode)缓存的积极性。默认值100,数值越高回收越激进。对于经常编译、频繁读写大量小文件的工作场景,适当提高它可以把更多内存让给真正的程序使用。
我个人的推荐是设成200,但并不是越高越好。如果你经常访问同一批目录(比如Web服务器反复读取无限多的静态文件),太激进会导致元数据刚被回收又要重新读磁盘,反而增加IO。普通桌面用户设成150-200体感都不错。
3.3min_free_kbytes:给系统兜底的内存水位线
这个参数是很多教程容易忽略的。它设置了内核保留空闲内存的最低水位线,避免在内存耗尽时连内核自己都拿不到可用页。计算公式建议为总内存的1%-2%。以32GB内存为例,可以设min_free_kbytes=524288(512MB),也就是总内存的1.5%左右。
echo "vm.min_free_kbytes=524288" | sudo tee -a /etc/sysctl.d/99-memory-tuning.conf需要注意,这个值宁可稍微高一点也不要太低。水位线太低时,内存压力来得太快,内核来不及做后台回收,就只能走直接回收路径,最容易触发卡顿和OOM。设太高也有副作用,对内存极小的设备来说会浪费可用空间。一个32GB的机器设置后,free -h里可以看到free部分会比之前多出几百MB,这是正常现象。
很多教程会把三个参数一次性塞给你,我只想说:别拿别人的参数直接套自己的机器。不同内存大小、不同磁盘类型、不同负载场景,最优参数组合一定不一样。最小改动原则是:先只调整swappiness和vfs_cache_pressure,观察一周,再决定要不要动min_free_kbytes。
4. zram登场:把交换从磁盘搬到内存压缩区,卡顿的直接解法
4.1 传统swap分区或swapfile为什么在桌面场景里救不了急
传统做法是划分一个swap分区,或者用fallocate做一个swapfile。这种方案的逻辑是“把不常用的内存页放到磁盘上”,释放物理内存给当前活跃进程用。但它有两个硬伤:
第一,磁盘IO速度远低于内存,即使是最好的NVMe SSD,随机读写也就几十万IOPS,而内存是纳秒级的。一旦系统开始频繁换页,swap读写就成了最大瓶颈。
第二,SSD有写入寿命限制,频繁换页会快速消耗闪存寿命。我用一块老的128G SSD做过测试,重度swap使用一个月就能看到SMART里的写入量飙升,虽然不至于立刻损坏,但看着心里发毛。
所以对桌面用户来说,swapfile不是不能用,而是应该作为“最后的兜底”,而不是日常换页的主力。
4.2 zram是什么:用CPU换速度,把交换页压缩后留在内存里
zram是在RAM里创建一个压缩块设备,内核把要换出的页面先压缩再存进去。这样做的最大价值是:swap操作不再依赖磁盘,速度接近内存,同时因为压缩率高(通常能把换出的页面压到原来的1/3到1/2),实际能用的“交换空间”比物理内存大不少。
代价是需要消耗CPU进行压缩。现在主流的CPU都有硬件加速指令集,配合lz4或zstd算法,压缩和解压的开销完全可以接受。我用4核笔记本实测,开启zram后编译大型项目,CPU占用率上升不到3%,但卡顿感几乎消失。
Ubuntu启用zram有几种方式。我最推荐用zram-tools包,简单直接:
sudo apt install zram-tools sudo dpkg-reconfigure zram-tools在配置界面里可以设置压缩算法为zstd,设置PERCENT=50。这表示zram大小是物理内存的50%。安装完成后,用zramctl就能看到压缩设备的信息:
zramctl如果你习惯用systemd原生的方式,也可以写/etc/systemd/zram-generator.conf:
[zram0] zram-size = min(ram / 2, 8192) compression-algorithm = zstdzram-size这个配置支持min()、max()这类表达式,上面这个写法意思是“取物理内存一半和8GB两者中的较小值”,适合内存比较大的机器。
4.3 zram和zswap怎么选:一张表讲清楚
很多人会把zram和zswap搞混,这俩名字像,机制不同。zswap是一个在内核里的压缩缓存层,它把要换出的页面先压缩,然后存放在RAM里的动态池中,如果池满了,再把旧页面真正写入磁盘swap。可以说zswap是“给swap加了一层压缩缓存”。
| 对比项 | zram | zswap |
|---|---|---|
| 本质 | 独立压缩块设备 | 磁盘swap的压缩缓存层 |
| 是否依赖磁盘swap | 不依赖 | 依赖底层磁盘swap |
| 适用场景 | 内存较小的桌面/嵌入式设备 | 已有磁盘swap且不想改布局的场景 |
| 推荐度(桌面) | 高 | 中 |
对大多数Ubuntu桌面用户来说,zram更省心,因为不用管磁盘swapfile的配置,直接占用内存里的固定空间。如果内存只有4GB,zram设成min(ram/2, 2048)比较合适,设太大会让物理内存更紧张,反而适得其反。
开启zram之后,记得检查一下free -h里的swap总量是不是变大了,同时用zramctl看compressed和data的比值。如果data远大于compressed,说明压缩算法工作良好;如果两者接近,说明你的内存页本身就很难压缩(比如已经被压缩过的数据),这时候再调整算法收益也不大。
5. OOM杀掉重要程序?从内核规矩讲到systemd-oomd
5.1 内核OOM killer是怎么选“牺牲者”的
别指望内核OOM killer聪明到帮你保IDE。内核通过给每个进程计算oom_score来选择牺牲者,基础分和进程内存占用正相关,同时可以通过oom_score_adj调整优先级。分数最高的进程会被杀,而oom_score_adj可以在/proc/<pid>/oom_score_adj里手动设置,取值从-1000到1000,-1000表示这个进程永远不会被OOM killer选中。
问题就是:浏览器、IDE、桌面环境这些大进程往往内存占用最高,OOM killer杀掉它们对释放内存最“见效”,但对你来说就是灾难。我曾经遇到过一个编辑器进程被OOM杀掉,辛辛苦苦写了一下午的笔记全部丢失。
5.2 用systemd保护关键服务,别让老大哥乱点名
如果你希望某个关键服务永远不被OOM杀掉,可以写systemd服务单元:
[Service] OOMScoreAdjust=-500或者对普通进程,临时用:
echo -500 | sudo tee /proc/<pid>/oom_score_adj负值代表降低被选中的概率。不过要小心,不能给太多进程都设负值,否则OOM killer只能找其他进程下手,等于强迫它去杀你没设保护的程序。
5.3 systemd-oomd:Ubuntu 22.04+自带的用户态内存守护
从systemd 247开始,systemd增加了oomd,Ubuntu 22.04及后续版本默认启用systemd-oomd.service。它的好处是比内核OOM killer更早介入,通过cgroup级别的内存压力和swap使用量判断,优先杀掉“大而不重要”的cgroup里的进程,而不是整个系统找最吃内存的进程杀掉。
检查它是否在运行:
systemctl status systemd-oomd它的配置文件在/etc/systemd/oomd.conf,一个常见配置长这样:
[OOM] SwapUsedLimit=90% DefaultMemoryPressureLimit=60%SwapUsedLimit=90%表示当swap使用率达到90%时,oomd开始kill占用最多的cgroup里的进程;DefaultMemoryPressureLimit=60%表示当cgroup内存压力累计值超过60%时,oomd也会介入。这样你可以给浏览器这类应用所在的cgroup单独配置更高的阈值,而不是一刀切。
需要提醒的是,systemd-oomd只对使用cgroup v2的slice生效,而且它主要关注系统的整体“健康度”,而不是具体恢复某个程序。如果你需要更针对性的保护,配合OOMScoreAdjust一起用更稳妥。
5.4 earlyoom作为备选方案
如果你的系统版本较老,或者你希望OOM处理更“主动”,可以尝试earlyoom。它是一个独立守护进程,常驻后台监控可用内存和swap,当内存降到预设阈值时,会优先杀掉得分最高的进程,并且允许通过配置文件忽略特定进程名,比如--ignore 'chromium|code'。
sudo apt install earlyoom sudo systemctl enable --now earlyoomearlyoom的优势是响应比内核OOM killer快得多,避免系统进入半死状态。劣势是它杀进程的逻辑也需要自己调,优先保护的进程名单要维护好。总的来说,我建议优先用systemd-oomd,因为它和系统组件配合更紧密;老版本或者特别网络环境受限的机器再考虑earlyoom。
6. 调完怎么算成功:压测方法与长期监控
6.1 用stress-ng主动制造内存压力,验证优化效果
调完参数之后最怕的是“感觉好像不卡了”这种无法量化的结论。我建议你主动压测。安装stress-ng:
sudo apt install stress-ng然后用下面这条命令制造约4GB的内存压力,持续120秒:
stress-ng --vm 4 --vm-bytes 2G --timeout 120s在压测期间,另开一个终端同时观察vmstat 1和free -h,重点看:si/so是不是被抑制了,available是不是还能维持在一个合理区间,系统是否还能正常切换窗口。
如果你配置了zram,压测时可以在第三个终端持续跑zramctl,看压缩设备有没有在被正常使用。我实测过的组合配置(swappiness=10+ zram + 32GB内存)在4GB压力下几乎稳定在“有少量swap换入但不再卡死”的状态,而同一台机器没调优前,2GB压力就能让UI卡得没法看。
6.2 长期监控:让数据告诉你是否需要再调
一次压测不代表长期效果。我建议你至少让系统运行一周,期间定期记录:
free -h里的available最低值journalctl -k | grep -i oom是否出现新的OOM记录systemctl status systemd-oomd有没有频繁触发kill
如果available在高峰期仍然长期低于1GB,说明要么内存还是不够,要么缓存回收策略仍然偏保守,可以考虑增加物理内存,而不是继续压榨参数。反过来,如果OOM日志彻底消失,卡顿感也消失,那这套配置基本可以固定下来。
6.3 针对不同机器的一张经验配置表
| 场景 | 内存大小 | swappiness | vfs_cache_pressure | 交换方案 | systemd-oomd |
|---|---|---|---|---|---|
| 老笔记本轻度办公 | 4GB | 10 | 150 | zram 2GB + 磁盘swapfile 2GB | 启用 |
| 日常开发机 | 16GB | 10 | 150 | zram 4GB | 启用 |
| 高性能工作站 | 32GB+ | 5-10 | 200 | 不配swap或配zRAM 8GB兜底 | 启用 |
| 服务器(web服务) | 8GB-32GB | 30 | 100 | swapfile 4GB或按需 | 根据业务选 |
服务器场景我特意把swappiness设高,是因为服务端通常更看重缓存命中率,而不是交互响应速度。这也是为什么我一直强调不能照搬——你的笔记本优化配置直接扔到服务器上,效果可能完全不同。
6.4 我踩过的那些坑,提前帮你避开
最后分享几个实际经验。
第一个坑是盲目设很大的zram。我之前在一台16GB的机器上把zram设成了12GB,结果物理内存被压缩设备占掉了一大块,普通负载看起来可用内存也不多,反而更卡。现在的建议是zram不要超过物理内存的50%,除非你确定自己的负载有很强的可压缩性。
第二个坑是给所有程序都设OOMScoreAdjust负值。这样做等于让OOM killer无处下刀,最后它只能去杀最关键的中间进程。保护的范围一定要小,控制在编辑器、浏览器这类不可替代的交互进程就行。
第三个坑是只调参不确认效果。参数调完之后至少要经过一次真实的高负载场景验证(压测、编译、开几十个标签页),而不是感觉“桌面流畅了点就叫优化完成”。判断的标准永远是数据:available是否撑得住、si/so是否明显下降、OOM记录是否消失。
这套流程走下来,Ubuntu的内存管理基本就能做到“平时安静、压力来时不乱、极限时保命”。每个参数都不是越多越好。动手改之前先把第2章的排查链路完整跑一遍,拿着数据去对照第6章的配置表做选择,效果会远超照着网上参数盲调。如果你调完一段时间后有了自己的实测数据,也欢迎回来交流你是在什么样的机器上改的、改了什么、体感如何。