当服务器突然变慢、进程莫名消失、甚至系统崩溃,很可能是 内存溢出(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.2G
  • available 列显示还剩多少可用内存,如果非常小(<100M)就危险了。
  • Swap 使用率很高(例如 used > 80%),也说明物理内存紧张。

2.2 查看内存占用最高的进程

top -o %MEM

M 键(大写)可以按内存使用率排序,一眼看到谁吃内存最多。

2.3 查看系统日志中的 OOM 记录

CentOS/RHEL 老版本:

grep -i "out of memory" /var/log/messages

Ubuntu/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 oom

3. 第二步:定位到具体“凶手”程序

找到 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_task

3.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=1

4.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=10

5. 第四步:针对数据库的内存溢出分析

MySQL 为例,哪些操作/配置容易导致内存爆炸?

5.1 配置不当 – 内存参数设置太大

参数说明危险设置
innodb_buffer_pool_sizeInnoDB 缓存数据和索引设置超过物理内存 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 BYDISTINCT 或子查询产生巨大临时表。
  • 频繁大字段查询SELECT * FROM huge_table 拉取 BLOB/TEXT 字段,全部存入内存结果集。

模拟案例
一个没有索引的 JOIN 语句,同时 100 个并发执行,每个 join_buffer_size = 32MB → 瞬间消耗 3.2GB 内存。

5.3 排查数据库内存溢出的步骤

  1. 开启慢查询日志,抓取执行时间长、扫描行数多的 SQL。
  2. 使用 SHOW PROCESSLIST 查看正在运行的 SQL 及其内存临时表使用情况。
  3. 监控 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

  1. 启用堆转储(Heap Dump)
    在 JVM 启动参数中加入:

    -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump

    当 OOM 发生时,会自动生成 .hprof 文件。

  2. 使用 MAT 或 VisualVM 分析堆转储

    • 找到 Dominator Tree 看哪个对象占内存最多。
    • 查看 GC Roots 路径,定位到哪个类/线程持有了引用。
  3. 实时监控 JVM 内存

    jstat -gcutil <PID> 1000  # 每秒打印 GC 情况

    如果 FGC 频繁且 Old Gen 持续增长,说明存在内存泄漏。

  4. 打印 OOM 时的堆栈

    -XX:OnOutOfMemoryError="kill -3 %p"   # 输出 thread dump

7. 总结与实战小贴士

✅ 排查内存溢出“三字经”

  1. 看日志dmesg + messages + syslog 找 OOM 记录。
  2. 定进程 – 从 OOM 日志中拿到 PID 和名字。
  3. 调内核overcommit_memoryswappinessoom_score_adj
  4. 析应用 – 数据库看慢 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 程序最好单独部署(或容器隔离),避免互相争抢内存。

内存溢出不可怕,怕的是不知道从哪里下手。 按照本文的步骤,你一定能快速定位到“凶手”,并给出有效的优化方案。祝你的服务器永远内存充足! 🎉

最后修改:2026 年 04 月 14 日
如果觉得我的文章对你有用,请随意赞赏