当服务器突然变慢、进程莫名消失、甚至系统崩溃,很可能是 内存溢出(OOM) 惹的祸。本文带你从零开始,一步步检查内存溢出、定位具体程序、优化内核参数,并分别针对数据库和 Java 程序分析常见“罪魁祸首”。
1. 什么是内存溢出
简单说,内存溢出就是程序向操作系统申请内存时,系统说“没有足够的内存给你用了”。
结果可能是:
- 申请内存失败,程序崩溃
- 操作系统启动 OOM Killer(内存杀手),强行杀掉占用内存最多的进程
就好比你往一个已经满了的杯子里继续倒水,水会溢出——内存“溢出”到其他不该占用的地方,系统就会乱套。
2. 第一步:检查服务器是否发生内存溢出
当怀疑服务器内存有问题时,先用几个命令快速“望闻问切”。
2.1 查看整体内存使用情况
free -h输出示例:
total used free shared buff/cache available
Mem: 7.6G 6.9G 0.2G 0.1G 0.5G 0.3G
Swap: 2.0G 1.8G 0.2Gavailable列显示还剩多少可用内存,如果非常小(<100M)就危险了。- Swap 使用率很高(例如 used > 80%),也说明物理内存紧张。
2.2 查看内存占用最高的进程
top -o %MEM按 M 键(大写)可以按内存使用率排序,一眼看到谁吃内存最多。
2.3 查看系统日志中的 OOM 记录
CentOS/RHEL 老版本:
grep -i "out of memory" /var/log/messagesUbuntu/Debian:
grep -i "out of memory" /var/log/syslog使用 dmesg 查看内核消息:
dmesg | grep -i "oom"如果看到类似 Out of memory: Kill process 12345 (java) score 987 or sacrifice child 的字样,说明已经发生过 OOM 事件。
2.4 查看历史 OOM 记录(systemd 系统)
journalctl -k | grep -i oom3. 第二步:定位到具体“凶手”程序
找到 OOM 记录后,里面会包含 进程 ID(PID) 和 进程名,例如:
Out of memory: Kill process 1892 (mysqld) score 987那么凶手就是 mysqld。
但有时进程已经被杀,如何找到它的“遗言”?
3.1 从 OOM 日志中提取详细信息
dmesg | grep -A 20 "Kill process"-A 20 会显示匹配行之后的 20 行,里面包含了该进程的内存映射、页表等。
3.2 查看被杀进程的启动命令
如果日志里只有 PID,可以通过以下方式反查(前提是进程还未完全消失或你记得时间):
ps -p <PID> -o cmd但 OOM 发生后进程已终止,所以最好在问题复现时立即监控。
更聪明的办法是开启内核参数记录 pid:
echo 1 > /proc/sys/kernel/print_oom_kill_task3.3 使用 smem 统计真实内存占用
top 中的 RES 可能包含共享内存,smem 可以更准确:
smem -r -s rss | head -20(需要先安装 smem)
4. 第三步:服务器内核参数优化
通过调整内核参数,可以降低 OOM 发生的概率,或者让系统行为更可控。
4.1 vm.overcommit_memory – 内存超售策略
内核允许程序申请比实际物理内存更大的虚拟内存,这叫 overcommit。
0(默认):启发式超售,内核自己判断是否允许。1:总是允许超售(风险大,但适合某些内存分配苛刻的应用)。2:禁止超售,申请的内存总和不能超过swap + 物理内存 * overcommit_ratio。
推荐设置(避免过度超售导致 OOM):
sysctl -w vm.overcommit_memory=2
sysctl -w vm.overcommit_ratio=80 # 最多使用物理内存的80%4.2 vm.panic_on_oom – OOM 时是否重启系统
0:触发 OOM Killer 杀进程(默认)1:直接内核 panic 重启系统(适合高可用环境,让集群切换)
sysctl -w vm.panic_on_oom=14.3 调整 OOM Killer 的评分机制
每个进程都有一个 oom_score(0~1000),分数越高越容易被杀。你可以手动调整某个进程的 adj 值:
echo -500 > /proc/<PID>/oom_score_adj负值表示降低被杀概率,正值表示提高。数据库或关键 Java 应用可以设置较小的 adj。
4.4 vm.swappiness – 控制 Swap 使用倾向
值越高(0~100),越积极使用 Swap。
对于内存敏感的数据库或 Java,建议降低 swappiness,避免频繁换页导致性能骤降:
sysctl -w vm.swappiness=105. 第四步:针对数据库的内存溢出分析
以 MySQL 为例,哪些操作/配置容易导致内存爆炸?
5.1 配置不当 – 内存参数设置太大
| 参数 | 说明 | 危险设置 |
|---|---|---|
innodb_buffer_pool_size | InnoDB 缓存数据和索引 | 设置超过物理内存 70% |
tmp_table_size / max_heap_table_size | 内存临时表上限 | 同时并发几百个连接,每个 64MB → 数 GB |
join_buffer_size | 每个 JOIN 操作的缓冲区 | 设置 256MB,100 个连接 → 25GB |
sort_buffer_size | 排序缓冲区 | 同上 |
检查当前配置:
SHOW VARIABLES LIKE '%buffer%';
SHOW VARIABLES LIKE '%tmp_table_size%';5.2 危险 SQL 操作
- 无索引的 JOIN:例如两张 1000 万行的表做笛卡尔积,临时结果集可能几十 GB。
- 对大量数据进行排序:
ORDER BY没有索引,MySQL 会使用磁盘临时表,但若sort_buffer_size过大,内存也会爆。 - 使用临时表存储中间结果:
GROUP BY、DISTINCT或子查询产生巨大临时表。 - 频繁大字段查询:
SELECT * FROM huge_table拉取 BLOB/TEXT 字段,全部存入内存结果集。
模拟案例:
一个没有索引的 JOIN 语句,同时 100 个并发执行,每个 join_buffer_size = 32MB → 瞬间消耗 3.2GB 内存。
5.3 排查数据库内存溢出的步骤
- 开启慢查询日志,抓取执行时间长、扫描行数多的 SQL。
- 使用
SHOW PROCESSLIST查看正在运行的 SQL 及其内存临时表使用情况。 监控
performance_schema中的内存统计:SELECT * FROM sys.memory_global_total; SELECT * FROM memory_by_thread_by_current_bytes;
6. 第五步:针对 Java 程序的内存溢出分析
Java 运行在 JVM 中,JVM 会向操作系统申请一块内存(堆 + 栈 + 元空间等)。Java 的 OOM 有两种:
- JVM 内部 OOM(堆内存不足):抛出
java.lang.OutOfMemoryError - 操作系统 OOM Killer:JVM 整体占用物理内存太多,被内核杀掉
6.1 常见导致 Java OOM 的代码场景
| 场景 | 示例代码 | 为什么爆内存 |
|---|---|---|
| 集合未清理(内存泄漏) | HashMap 只添加不删除,且作为静态变量 | 对象无法 GC,堆持续增长 |
| 无限循环创建对象 | while(true) { list.add(new byte[1MB]); } | 瞬间填满堆 |
| 大对象直接分配 | new byte[Integer.MAX_VALUE] | 直接请求超大内存,超出堆限制 |
| 线程栈溢出 | 递归没有终止条件 | 每个方法调用占用栈帧,导致 StackOverflowError 或栈内存耗尽 |
| 不合理使用缓存 | 用 HashMap 做缓存,无大小限制 | 缓存无限膨胀 |
| 每个请求都加载超大文件 | Files.readAllBytes() 一个 500MB 文件 | 高并发下多个请求同时加载,堆爆炸 |
6.2 JVM 参数设置不当
-Xmx设置过小(比如 512MB),但业务需要 2GB。-Xmx设置过大(比如 8GB),而物理内存只有 8GB,其他进程无内存可用 → 触发系统 OOM。- 元空间(Metaspace)未限制(
-XX:MaxMetaspaceSize),动态生成大量类(如反射、CGLIB)导致元空间溢出。
6.3 如何定位 Java OOM
启用堆转储(Heap Dump)
在 JVM 启动参数中加入:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump当 OOM 发生时,会自动生成
.hprof文件。使用 MAT 或 VisualVM 分析堆转储
- 找到
Dominator Tree看哪个对象占内存最多。 - 查看
GC Roots路径,定位到哪个类/线程持有了引用。
- 找到
实时监控 JVM 内存
jstat -gcutil <PID> 1000 # 每秒打印 GC 情况如果
FGC频繁且Old Gen持续增长,说明存在内存泄漏。打印 OOM 时的堆栈
-XX:OnOutOfMemoryError="kill -3 %p" # 输出 thread dump
7. 总结与实战小贴士
✅ 排查内存溢出“三字经”
- 看日志 –
dmesg+messages+syslog找 OOM 记录。 - 定进程 – 从 OOM 日志中拿到 PID 和名字。
- 调内核 –
overcommit_memory、swappiness、oom_score_adj。 - 析应用 – 数据库看慢 SQL 和 buffer 配置;Java 看堆转储和 GC 日志。
🧰 常用命令速查表
| 目的 | 命令 | |
|---|---|---|
| 查看内存总量 | free -h | |
| 动态看内存前几名 | top -o %MEM | |
| 搜索 OOM 日志 | `dmesg \ | grep -i oom` |
| 查看 MySQL 内存配置 | SHOW VARIABLES LIKE '%buffer%'; | |
| 查看 Java 堆大小 | jinfo -flag MaxHeapSize <PID> | |
| 生成 Java 堆转储 | jmap -dump:live,format=b,file=heap.hprof <PID> |
🚨 最后的小提醒
- 不要盲目增加内存,先从代码和配置找原因。
- 生产环境调整内核参数前,先在测试环境验证。
- 数据库和 Java 程序最好单独部署(或容器隔离),避免互相争抢内存。
内存溢出不可怕,怕的是不知道从哪里下手。 按照本文的步骤,你一定能快速定位到“凶手”,并给出有效的优化方案。祝你的服务器永远内存充足! 🎉