即使内存管理机制再完善,当物理内存被耗尽时,Linux 内核必须做出抉择:杀掉哪个进程来释放内存? 这个“挥刀”的决定由 OOM Killer 执行。本文深入讲解 OOM Killer 的评分机制、如何保护关键进程不被误杀、如何分析 OOM 日志定位根因,以及 Cgroups v2 下的内存限制策略。同时,介绍内存碎片问题与回收机制,帮你建立完整的内存故障排查能力。
一、OOM Killer:内存耗尽时的“最后裁决”
当系统物理内存(包括 Swap)耗尽时,内核会触发 OOM Killer(Out-Of-Memory Killer) ,选择一个进程杀掉,以释放内存,保证系统核心功能继续运行。
1.1 OOM Killer 的评分机制
OOM Killer 通过为每个进程计算一个 “坏蛋分数”(OOM Score) 来决定杀谁。分数越高,越容易被选中。计算基于以下因素:
查看进程的 OOM 分数:
# 查看所有进程的 OOM 分数cat/proc/*/oom_score|sort-nr|head-20# 查看特定进程的分数cat/proc/1234/oom_score分数范围 0 到 1000(实际上可能更高),分数最高的进程被选为牺牲品。
1.2 保护关键进程:调整 oom_score_adj
通过调整 /proc//oom_score_adj,可以影响进程的 OOM 分数。
示例:保护 MySQL 不被 OOM Kill
# 查找 MySQL 的 PIDpgrep mysqld# 假设 PID 为 1234# 设置 oom_score_adj 为 -1000(禁止 OOM Kill)echo-1000>/proc/1234/oom_score_adj# 验证cat/proc/1234/oom_score_adj在 systemd 服务中配置:
在服务单元文件中添加:
ini
[Service]
设置 OOM 保护(值范围 -1000 到 1000)
OOMScoreAdjust=-500
1.3 OOM 日志分析
当 OOM Killer 触发时,内核会输出详细的日志到 dmesg 或 /var/log/messages。这是定位内存问题的第一手资料。
查看 OOM 日志:
# 查看最近的内核 OOM 事件dmesg|grep-i"out of memory"-A20# 或从 systemd 日志中查找journalctl-k|grep-i"out of memory"-A20典型 OOM 日志解读:
[12345.678]Out of memory: Kill process9876(mysqld)score956or sacrifice child[12345.679]Killed process9876(mysqld)total-vm:12345678kB, anon-rss:8765432kB, file-rss:0kB[12345.680]oom_reaper: reaped process9876, now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB关键信息:
Kill process 9876 (mysqld):被杀的进程是 MySQL。
score 956:该进程的 OOM 分数极高。
total-vm:进程的总虚拟内存。
anon-rss:匿名内存(进程私有内存)的大小。
如果日志中出现 MEMCG 字样,说明是 Cgroups 层级触发的 OOM(容器内存限制),而非宿主机全局 OOM。
二、内存碎片与回收机制
2.1 内存碎片问题
长期运行的系统会产生内存碎片——物理内存被分割成许多不连续的小块,即使有足够的总空闲内存,也可能无法分配一个大块的连续内存(如需要 2MB 连续内存的大页)。
# 查看内存碎片情况cat/proc/buddyinfo输出示例:
text
Node 0, zone Normal 1 2 4 8 16 32 64 128 256 512 1024
数字从左到右表示空闲页块的大小(2^order 页)。如果左边的数字很大,右边的数字为 0,说明存在碎片化——有大量小内存块,但没有大块连续内存。
2.2 内存回收触发
当内存低于阈值时,内核会启动回收机制,释放 Page Cache 和 Swap Cache。
# 查看内存回收统计cat/proc/vmstat|grep-E'pgscan|pgsteal'pgscan_kswapd:kswapd 内核线程扫描的页数。
pgsteal_kswapd:kswapd 成功回收的页数。
如果 pgscan 远大于 pgsteal,说明回收效率低,可能需要增加内存或调整 swappiness。
三、Cgroups v2 内存限制
在容器环境中,内存限制通常通过 Cgroups v2 实现。相比 v1,v2 提供了更一致的内存管理接口。
3.1 Cgroups v2 内存核心文件
3.2 容器内存 OOM 排查
当容器触发 OOM 时,可以用以下命令查看:
# 查看容器的内存限制和使用量cat/sys/fs/cgroup/memory.maxcat/sys/fs/cgroup/memory.currentcat/sys/fs/cgroup/memory.events# 查看 oom 事件计数如果 memory.events 中的 oom 计数不为 0,说明该容器曾触发 OOM。
3.3 Kubernetes 中的内存 OOM
K8s 中 Pod 因内存超限被杀,会显示 OOMKilled 状态:
kubectl describe pod my-pod输出中会包含:
text
State: Failed
Reason: OOMKilled
解决方案:
增加 Pod 的 memory.limits。
排查应用内存泄漏(使用 jmap、pprof 等工具)。
优化应用内存使用(如减少缓存、调整 GC 参数)。
四、实战:Java 应用内存泄漏排查
Java 应用是内存问题的“重灾区”,因为它运行在 JVM 中,内存管理涉及 JVM 堆和操作系统内存两个层面。
4.1 典型场景
现象:Java 应用的容器内存持续增长,最终触发 OOMKilled。
排查步骤:
第 1 步:确认是否 JVM 堆内泄漏
# 进入容器kubectlexec-itmy-pod -- /bin/bash# 使用 jcmd 查看堆使用情况jcmd<pid>GC.heap_info# 或使用 jstat 观察 GC 行为jstat-gc<pid>1s如果 GC.heap_info 显示 Old Gen 持续增长,且 GC 无法回收,说明堆内泄漏。
第 2 步:获取堆转储(Heap Dump)
# 生成堆转储文件jcmd<pid>GC.heap_dump /tmp/heap.hprof# 或使用 jmapjmap -dump:live,format=b,file=/tmp/heap.hprof<pid>第 3 步:分析堆转储
将 heap.hprof 下载到本地,使用 MAT(Memory Analyzer Tool) 或 JVisualVM 分析。
MAT 的 Leak Suspects Report 可以直接告诉你哪个对象占用了最多的内存,以及它的引用链。
4.2 非堆内存泄漏(Direct Memory / Native Memory)
如果堆内存正常,但容器内存仍然不断增长,可能是 Direct Memory 或 Native Memory 泄漏。
Direct Memory:使用 ByteBuffer.allocateDirect 分配的内存,不受 JVM 堆大小限制。
Native Memory:JNI 调用或底层 C 库分配的内存。
排查 Native Memory 泄漏,可以使用 pmap 观察进程的内存段变化,或使用 jemalloc 的 profiling 功能。
五、内存调优实践
# 临时调整sudosysctlvm.swappiness=10# 永久调整(写入 /etc/sysctl.conf)vm.swappiness=105.2 调整 overcommit 策略
/proc/sys/vm/overcommit_memory 控制内核是否允许超额分配内存:
对于数据库服务器,建议设置为 2(严格限制),避免因超额分配导致 OOM。
echo2>/proc/sys/vm/overcommit_memory配合 overcommit_ratio(默认 50%),总可分配内存为 物理内存 * overcommit_ratio / 100 + Swap。
5.3 使用 numactl 优化 NUMA 内存访问
# 查看 NUMA 拓扑numactl--hardware# 将进程绑定到 NUMA 节点 0 的 CPU 和内存numactl--cpunodebind=0--membind=0./myapp六、小结
OOM Killer 是内存耗尽的最终防线,通过 oom_score 决定杀谁。用 oom_score_adj 保护关键进程。
Cgroups v2 提供了容器的内存限制:memory.max(硬限制)、memory.high(软限制)。
内存碎片通过 /proc/buddyinfo 查看,碎片严重会影响大页分配。
Java 内存泄漏的常规定位路径:确认堆内泄漏 → 获取堆转储 → 使用 MAT 分析。