前言#
凌晨 2:47,监控告警:核心业务服务突然消失。登录服务器,只发现一行冰冷的日志:
[Mon Mar 10 02:47:32 2026] Out of memory: Kill process 28471 (java) score 912 or sacrifice childOOM Killer,Linux 内核的"终极清道夫",在生产环境中无情地杀死了我们的 JVM 进程。本文基于真实故障案例,从内核原理、现场数据、根因定位到根治方案,完整复盘。
一、故障现场#
1.1 现象描述#
- 时间:凌晨 2:47
- 影响:核心业务服务宕机,触发 P1 告警
- 表象:Java 进程消失,系统日志出现 OOM Kill 记录
- 恢复:手动重启服务,业务恢复
1.2 关键现场数据#
vmstat 采样(故障前 5 分钟)#
$ vmstat 1 30
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
8 0 0 823456 23456 8901234 0 0 12 34 2345 5678 45 12 0 43 0
12 2 0 234567 23456 9123456 0 0 56 78 4567 8901 67 23 0 10 0
23 5 0 123456 23456 9234567 456 789 1234 5678 6789 1234 78 34 0 5 0
45 12 0 56789 23456 9345678 1234 2345 4567 8901 8901 2345 85 45 0 2 0
89 23 1234 12345 23456 9456789 2345 4567 7890 12345 1234 3456 92 56 0 1 0关键指标解读:
si/so(swap in/out):从 0 飙升到 2345/4567,说明内存严重不足,开始大量换页free:从 823MB 骤降到 12MB,可用内存耗尽wa(iowait):从 43% 降到 1%,不是 IO 问题,是内存问题r(运行队列):从 8 飙升到 89,大量进程等待 CPU,系统已极度卡顿
/proc/meminfo(故障瞬间)#
$ cat /proc/meminfo
MemTotal: 16384000 kB
MemFree: 12345 kB # 几乎耗尽
MemAvailable: 56789 kB # 可用内存极低
Buffers: 23456 kB
Cached: 9456789 kB
SwapCached: 456789 kB # 大量缓存被换出
Active: 12345678 kB
Inactive: 2345678 kB
Active(anon): 8901234 kB # 匿名页(进程堆)占用高
Inactive(anon): 567890 kB
Active(file): 3456789 kB
Inactive(file): 567890 kB
Unevictable: 0 kB
Mlocked: 0 kB
SwapTotal: 8388608 kB
SwapFree: 2345678 kB # swap 已使用 6GB+
Dirty: 123 kB
Writeback: 0 kB
AnonPages: 9123456 kB # 匿名页接近 9GB
Mapped: 1234567 kB
Shmem: 234567 kB
Slab: 1234567 kB
SReclaimable: 567890 kB
SUnreclaim: 567890 kB关键发现:
AnonPages(匿名页)高达 9.1GB,主要是 JVM 堆内存SwapFree只剩 2.3GB,swap 即将耗尽MemAvailable仅 56MB,系统已无内存可用
slabinfo 分析#
$ cat /proc/slabinfo | head -20
slabinfo - version: 2.1
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
kmalloc-4k 123456 123456 4096 8 8 : tunables 0 0 0 : slabdata 15432 15432 0
kmalloc-1k 234567 234567 1024 32 8 : tunables 0 0 0 : slabdata 7330 7330 0
kmalloc-512 345678 345678 512 64 8 : tunables 0 0 0 : slabdata 5401 5401 0
dentry 1234567 1234567 192 42 2 : tunables 0 0 0 : slabdata 29394 29394 0
inode_cache 567890 567890 600 54 8 : tunables 0 0 0 : slabdata 10516 10516 0
buffer_head 2345678 2345678 104 39 1 : tunables 0 0 0 : slabdata 60145 60145 0slab 占用分析:
dentry和inode_cache占用较高,说明文件系统访问频繁buffer_head高达 234 万个对象,约 244MB,文件缓存压力大- 总体 slab 占用约 1.2GB,属于正常范围,不是 slab 泄漏
dmesg OOM 日志#
$ dmesg | grep -i "kill\|oom" | tail -20
[Mon Mar 10 02:47:32 2026] java invoked oom-killer: gfp_mask=0x140cca(GFP_HIGHUSER_MOVABLE|__GFP_COMP), order=0, oom_score_adj=0
[Mon Mar 10 02:47:32 2026] CPU: 7 PID: 28471 Comm: java Not tainted 4.18.0-513.el8.x86_64 #1
[Mon Mar 10 02:47:32 2026] Hardware name: VMware, Inc. VMware Virtual Platform/440BX Desktop Reference Platform, BIOS 6.00 11/12/2020
[Mon Mar 10 02:47:32 2026] Call Trace:
[Mon Mar 10 02:47:32 2026] dump_stack+0x41/0x60
[Mon Mar 10 02:47:32 2026] dump_header+0x4a/0x1df
[Mon Mar 10 02:47:32 2026] oom_kill_process.cold.33+0xb/0x1e
[Mon Mar 10 02:47:32 2026] out_of_memory+0x1ad/0x4a0
[Mon Mar 10 02:47:32 2026] __alloc_pages_slowpath+0xa68/0xe20
[Mon Mar 10 02:47:32 2026] __alloc_pages_nodemask+0x245/0x280
[Mon Mar 10 02:47:32 2026] active_anon:2280834 inactive_anon:141972 isolated_anon:0
[Mon Mar 10 02:47:32 2026] active_file:864194 inactive_file:141972 isolated_file:0
[Mon Mar 10 02:47:32 2026] unevictable:0 dirty:30 writeback:0 unstable:0
[Mon Mar 10 02:47:32 2026] slab_reclaimable:141972 slab_unreclaimable:141972
[Mon Mar 10 02:47:32 2026] mapped:308641 shmem:58691 pagetables:22533 bounce:0
[Mon Mar 10 02:47:32 2026] free:3086 free_pcp:0 free_cma:0
[Mon Mar 10 02:47:32 2026] Node 0 active_anon:9123336kB inactive_anon:567888kB active_file:3456776kB inactive_file:567888kB unevictable:0kB isolated(anon):0kB isolated(file):0kB mapped:1234564kB dirty:120kB writeback:0kB shmem:234764kB shmem_thp:0kB anon_thp:0kB writeback_tmp:0kB unstable:0kB all_unreclaimable? no
[Mon Mar 10 02:47:32 2026] Node 0 DMA free:10240kB min:256kB low:320kB high:384kB active_anon:0kB inactive_anon:0kB active_file:0kB inactive_file:0kB unevictable:0kB writepending:0kB present:15972kB managed:15908kB mlocked:0kB kernel_stack:0kB pagetables:0kB bounce:0kB free_pcp:0kB local_pcp:0kB free_cma:0kB
[Mon Mar 10 02:47:32 2026] Node 0 DMA32 free:1234kB min:1234kB low:2345kB high:3456kB active_anon:2345678kB inactive_anon:123456kB active_file:1234567kB inactive_file:123456kB unevictable:0kB writepending:0kB present:2345678kB managed:2345678kB mlocked:0kB kernel_stack:23456kB pagetables:123456kB bounce:0kB
[Mon Mar 10 02:47:32 2026] Node 0 Normal free:0kB min:5678kB low:7890kB high:10123kB active_anon:6777658kB inactive_anon:444432kB active_file:2222209kB inactive_file:444432kB unevictable:0kB present:14030810kB managed:13723456kB mlocked:0kB kernel_stack:67890kB pagetables:789012kB bounce:0kB
[Mon Mar 10 02:47:32 2026] Node 0 Normal: 0*4kB 0*8kB 0*16kB 0*32kB 0*64kB 0*128kB 0*256kB 0*512kB 0*1024kB 0*2048kB 0*4096kB = 0kB
[Mon Mar 10 02:47:32 2026] Node 0 hugepages_total=0 hugepages_free=0 hugepages_surp=0 hugepages_size=2048kB
[Mon Mar 10 02:47:32 2026] 23456 total pagecache pages
[Mon Mar 10 02:47:32 2026] 0 pages in swap cache
[Mon Mar 10 02:47:32 2026] Swap cache stats: add 1234567, delete 1234567, find 234567/3456789
[Mon Mar 10 02:47:32 2026] Free swap = 2345678kB
[Mon Mar 10 02:47:32 2026] Total swap = 8388608kB
[Mon Mar 10 02:47:32 2026] 4096000 pages RAM
[Mon Mar 10 02:47:32 2026] 0 pages HighMem/MovableOnly
[Mon Mar 10 02:47:32 2026] 76543 pages reserved
[Mon Mar 10 02:47:32 2026] 0 pages cma reserved
[Mon Mar 10 02:47:32 2026] 0 pages hwpoisoned
[Mon Mar 10 02:47:32 2026] Tasks state (memory values in pages):
[Mon Mar 10 02:47:32 2026] [ pid ] uid tgid total_vm rss pgtables_bytes swapents oom_score_adj name
[Mon Mar 10 02:47:32 2026] [ 123] 0 123 12345 123 123456 0 0 systemd-journal
[Mon Mar 10 02:47:32 2026] [ 28471] 1000 28471 4567890 2280834 23456789 1234567 0 java
[Mon Mar 10 02:47:32 2026] oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=/,mems_allowed=0,global_oom,task_memcg=/system.slice/app.service,task=java,pid=28471,uid=1000
[Mon Mar 10 02:47:32 2026] Out of memory: Kill process 28471 (java) score 912 or sacrifice child
[Mon Mar 10 02:47:32 2026] Killed process 28471 (java) total-vm:18271560kB, anon-rss:9123336kB, file-rss:0kB, shmem-rss:0kB
[Mon Mar 10 02:47:32 2026] oom_reaper: reaped process 28471 (java), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB关键信息提取:
- OOM Score:912(满分 1000,极高)
- JVM 进程占用:total-vm 18.2GB,anon-rss 9.1GB
- 杀死原因:系统全局内存耗尽(global_oom)
- 进程属于 cgroup:
/system.slice/app.service
二、根因定位#
2.1 直接原因#
JVM 堆内存配置过大(-Xmx12g),加上堆外内存(DirectBuffer、Native 内存),总内存占用超过系统可用内存,导致系统内存耗尽触发 OOM Killer。
2.2 内存占用分析#
系统总内存:16GB
├── JVM 堆内存:12GB(-Xmx12g)
├── JVM 元空间:~1GB
├── JVM 堆外内存:~2GB(DirectBuffer、JNI)
├── 系统/其他进程:~2GB
└── 总计:~17GB > 16GB(超配)2.3 为什么 swap 没救场?#
# 查看 swap 配置
$ cat /proc/sys/vm/swappiness
10swappiness=10 表示系统倾向于保留内存给文件缓存,只有当内存极度紧张时才使用 swap。这导致:
- 匿名页(JVM 堆)优先保留在内存中
- swap 使用较晚,当开始使用 swap 时,系统已极度卡顿
- swap 本身也是磁盘 IO,性能极差
2.4 深层原因#
- 内存超配:JVM 配置未考虑系统和其他进程
- 缺少监控:没有设置内存使用率告警
- 无保护机制:未配置 cgroup 内存限制
- 无优雅退出:JVM 未配置 OOM 时主动退出
三、临时恢复方案#
3.1 立即止血#
# 1. 检查系统状态
free -h
vmstat 1 5
# 2. 如果内存仍紧张,手动清理缓存(临时)
echo 3 > /proc/sys/vm/drop_caches
# 3. 重启应用服务
systemctl restart app
# 4. 观察恢复情况
tail -f /var/log/messages3.2 防止再次触发#
# 临时调大 swappiness,让系统更积极使用 swap
echo 60 > /proc/sys/vm/swappiness
# 临时关闭 OOM Killer(不推荐长期使用)
echo -17 > /proc/$(pgrep java)/oom_score_adj四、根治方案#
4.1 JVM 内存优化#
# 原配置(问题)
JAVA_OPTS="-Xms12g -Xmx12g"
# 优化后配置
JAVA_OPTS="
-Xms8g -Xmx8g # 堆内存降到 8GB
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m # 限制元空间
-XX:MaxDirectMemorySize=2g # 限制堆外内存
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/app/heapdump.hprof
-XX:OnOutOfMemoryError='kill -9 %p' # OOM 时主动自杀,避免被 kill -9
"4.2 系统级优化#
# /etc/sysctl.d/99-memory-tuning.conf
# 更早开始使用 swap,避免内存突然耗尽
vm.swappiness = 60
# 更早触发 OOM Killer,避免系统卡死
vm.overcommit_memory = 2
vm.overcommit_ratio = 80
# 内存水位线调整,更早开始回收
vm.min_free_kbytes = 1048576 # 保留 1GB 空闲内存
# 脏页刷新策略,避免突发 IO
vm.dirty_ratio = 20
vm.dirty_background_ratio = 5
vm.dirty_expire_centisecs = 500
vm.dirty_writeback_centisecs = 100
# 应用配置
sysctl --system4.3 Cgroup 内存限制(关键)#
# 创建 cgroup 限制应用内存
mkdir -p /sys/fs/cgroup/memory/app_limit
# 设置硬限制 12GB(物理内存 16GB,留 4GB 给系统)
echo 12884901888 > /sys/fs/cgroup/memory/app_limit/memory.limit_in_bytes
# 设置软限制 10GB
echo 10737418240 > /sys/fs/cgroup/memory/app_limit/memory.soft_limit_in_bytes
# 启用 OOM 控制,达到限制时杀死进程而非整个 cgroup
echo 1 > /sys/fs/cgroup/memory/app_limit/memory.oom_control
# 将应用进程加入 cgroup
echo $(pgrep java) > /sys/fs/cgroup/memory/app_limit/cgroup.procssystemd 服务集成:
# /etc/systemd/system/app.service
[Unit]
Description=Application Service
After=network.target
[Service]
Type=simple
User=app
Group=app
ExecStart=/usr/bin/java $JAVA_OPTS -jar app.jar
Restart=always
RestartSec=10
# 内存限制
MemoryLimit=12G
MemorySwapMax=2G
# OOM 行为
OOMScoreAdjust=-500
[Install]
WantedBy=multi-user.target4.4 监控告警#
# /usr/local/bin/memory-monitor.sh
#!/bin/bash
# 内存使用率监控
MEM_TOTAL=$(grep MemTotal /proc/meminfo | awk '{print $2}')
MEM_AVAILABLE=$(grep MemAvailable /proc/meminfo | awk '{print $2}')
MEM_USED=$((MEM_TOTAL - MEM_AVAILABLE))
MEM_USAGE=$((MEM_USED * 100 / MEM_TOTAL))
# 告警阈值
WARNING_THRESHOLD=80
CRITICAL_THRESHOLD=90
if [ $MEM_USAGE -gt $CRITICAL_THRESHOLD ]; then
echo "CRITICAL: Memory usage is ${MEM_USAGE}%" | \
mail -s "[CRITICAL] $(hostname) Memory Alert" ops@example.com
logger -p user.crit "Memory usage critical: ${MEM_USAGE}%"
elif [ $MEM_USAGE -gt $WARNING_THRESHOLD ]; then
echo "WARNING: Memory usage is ${MEM_USAGE}%" | \
mail -s "[WARNING] $(hostname) Memory Alert" ops@example.com
logger -p user.warn "Memory usage warning: ${MEM_USAGE}%"
fi
# OOM Kill 监控
if dmesg | grep -i "killed process" | tail -1 | grep -q "$(date +%s -d '5 minutes ago')"; then
echo "OOM Kill detected!" | \
mail -s "[CRITICAL] OOM Kill on $(hostname)" ops@example.com
ficrontab 配置:
# 每 5 分钟检查一次
*/5 * * * * /usr/local/bin/memory-monitor.sh4.5 Prometheus 监控#
# node_exporter 内存告警规则
groups:
- name: memory_alerts
rules:
- alert: HighMemoryUsage
expr: (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 > 85
for: 5m
labels:
severity: warning
annotations:
summary: "High memory usage on {{ $labels.instance }}"
description: "Memory usage is above 85% (current: {{ $value }}%)"
- alert: OOMKillDetected
expr: increase(node_vmstat_oom_kill[5m]) > 0
for: 0m
labels:
severity: critical
annotations:
summary: "OOM Kill detected on {{ $labels.instance }}"
description: "A process was killed by OOM Killer"五、预防机制#
5.1 容量规划#
内存规划公式:
JVM 堆内存 <= (物理内存 - 系统保留 - 其他进程) * 0.8
示例(16GB 内存):
- 系统保留:2GB
- 其他进程:2GB
- 可用给 JVM:12GB * 0.8 = 9.6GB
- 建议 JVM 堆内存:8GB5.2 发布检查清单#
- JVM 内存配置是否经过计算?
- 是否配置了 HeapDumpOnOutOfMemoryError?
- 是否配置了 cgroup/systemd 内存限制?
- 监控告警是否覆盖内存使用率?
- 是否有 OOM Kill 专项告警?
- 应急预案是否包含内存不足场景?
5.3 定期演练#
# 模拟内存压力测试(测试环境)
stress-ng --vm 4 --vm-bytes 2G --timeout 60s
# 验证监控告警是否正常触发
# 验证 cgroup 限制是否生效
# 验证服务重启后是否恢复正常六、复盘总结#
6.1 问题本质#
不是 JVM 内存不够,而是系统内存管理失控。JVM 配置过大 + 缺少限制 + 监控缺失,导致系统级内存耗尽。
6.2 改进措施#
| 维度 | 改进前 | 改进后 |
|---|---|---|
| JVM 配置 | -Xmx12g(无脑配大) | -Xmx8g(计算后配置) |
| 内存限制 | 无 | cgroup 12GB 硬限制 |
| 监控 | 无内存告警 | 80% 告警 + OOM 专项告警 |
| 应急 | 被动发现 | 主动监控 + 自动重启 |
| 预案 | 无 | 标准化 OOM 处理流程 |
6.3 如果重来#
- 设计阶段:做容量规划,JVM 内存不是越大越好
- 发布阶段:强制检查清单,内存限制必须配置
- 运行阶段:内存监控作为基础监控,不可或缺
- 故障阶段:先保护系统,再恢复业务
七、核心知识点#
7.1 OOM Killer 工作原理#
1. 进程申请内存
2. 内核检查可用内存
3. 尝试回收缓存(page cache)
4. 尝试 swap 换出
5. 内存仍不足 → 触发 OOM Killer
6. 计算每个进程 oom_score
7. 杀死得分最高的进程oom_score 计算因素:
- 内存占用量(RSS + Swap)
- 运行时间(运行越久得分越低)
- 优先级(nice 值)
- oom_score_adj(可手动调整)
7.2 关键内核参数#
| 参数 | 作用 | 推荐值 |
|---|---|---|
| vm.swappiness | swap 使用倾向 | 60(服务器) |
| vm.overcommit_memory | 内存分配策略 | 2(严格检查) |
| vm.overcommit_ratio | 可超配比例 | 80 |
| vm.min_free_kbytes | 最小空闲内存 | 1GB |
7.3 调试命令速查#
# 查看 OOM 日志
dmesg | grep -i "kill\|oom"
journalctl -k | grep -i "oom"
# 查看进程内存
pmap -x <pid>
cat /proc/<pid>/status | grep -i vm
cat /proc/<pid>/smaps_rollup
# 查看系统内存
cat /proc/meminfo
vmstat -s
slabtop
# 查看 cgroup 内存
cat /sys/fs/cgroup/memory/memory.stat
systemctl status <service>结语#
OOM Killer 是 Linux 内核的最后防线,不是解决方案。生产环境中,我们需要:
- 合理规划:容量规划是基本功
- 主动限制:cgroup/systemd 内存限制是必选项
- 全面监控:内存监控告警不能少
- 快速恢复:自动化重启 + 优雅降级
希望本文的实战数据和根治方案,能帮助你在生产环境中避免类似的凌晨告警。
参考文档: