服务器内存明明还有好几个 G 空闲,程序却申请不到一块连续的大内存,报 Cannot allocate memory 或者 OOM;又或者跑了几天之后,free -h 里的可用内存越来越少,重启服务就恢复了。这类"看似有内存却用不了"的现象,根子往往在内存碎片上。
内存碎片是个容易被忽视但影响很实在的问题。它分两大类:一类是物理内存碎片,发生在内核的页分配层面;另一类是虚拟内存碎片,发生在进程地址空间层面。两者机理不同、表现不同、解决办法也不同。这篇文章把它们拆开讲清楚,再配上 Linux 上查看碎片的命令和缓解手段。
适合读者:负责高负载服务器、长时间运行的数据库/缓存/计算服务、遇到过"有内存却分配失败"或"内存慢慢涨"的运维和开发同学。读完你应该能判断:内存问题到底是真不够,还是碎掉的。
一、内存碎片的两种类型
先说清楚概念,这是后续所有分析的根基。
外部碎片:空闲内存总量够,但都是零散的小块,凑不出一块连续的大内存。你要 1GB 连续空间,系统手里有 1000 个 1MB 的小块,加起来有 1GB,但给不出连续的一块。
内部碎片:分配给程序的块比实际需要的大,多出来的部分被浪费在块内部,用不上也还不了。比如你申请 100 字节,分配器实际给了 128 字节的块,那 28 字节就是内部碎片。
从内核视角看,物理内存以"页"(page,通常 4KB)为最小单位管理。内核为了高效分配,采用了伙伴系统(buddy system):把内存按 2 的幂次分组成块(4KB、8KB、16KB……直到几 MB),分配时找最合适的块,不够就分裂大块。长期运行后,大块被不断分裂、释放、再分裂,就难以重新合并成连续大块——这就是内核层面物理碎片的来源。
从进程视角看,堆内存由分配器(glibc 的 malloc 用的 ptmalloc,或 jemalloc、tcmalloc 等)管理。程序反复申请释放不同大小的对象,堆里就会出现空洞,即使总空闲量够,也可能因为空洞零散而无法满足大块需求。
二、怎么量化内存碎片
不要凭感觉,用数据说话。
# 1. 看整体内存概况
free -h
# available 是关键:它估算的是"还能给新程序用的内存",不是简单的 free+buff/cache
# 2. 看内核物理内存碎片程度(重点)
cat /proc/buddyinfo
# 输出示例:
# Node 0, zone DMA 1 1 0 0 0 0 0 0 1 2 1
# Node 0, zone DMA32 100 200 150 80 40 10 3 0 0 0 0
# Node 0, zone Normal 5000 3000 2000 1000 100 20 5 1 0 0 0
# 每一列表示对应 order 的空闲块数量。order 越大块越大(2^order 页)。
# order 0 = 1 页(4KB),order 10 = 1024 页(4MB)。
# 如果高 order(比如 order 9、10)都是 0,说明缺大块连续内存,碎片严重
# 3. 看各 zone 的碎片化情况
cat /proc/pagetypeinfo
# 能看到不同迁移类型(Movable/Unmovable/Reclaimable 等)的块分布
# 4. 计算碎片指数(示意)
# 若 order 0 的空闲页远多于高 order 的空闲页,碎片就重
/proc/buddyinfo 是判断内核物理碎片的最重要文件。经验判断:如果 order 0、1 有很多空闲块,但 order 9、10 长期为 0,说明内存被切碎了,需要大页连续内存的应用(如某些驱动、虚拟化、大页优化场景)就容易分配失败。
# 5. 看系统级内存统计与碎片相关指标
cat /proc/meminfo | grep -iE "MemFree|MemAvailable|CmaFree|VmallocUsed|PageTables"
# VmallocUsed 持续增长可能意味着 vmalloc 空间的碎片或泄漏
# 6. 检查内存水位
cat /proc/zoneinfo | grep -A5 "Node 0, zone"
进程层面的碎片,看堆分配器的统计(以 glibc 的 malloc 统计为例,需要程序调用 malloc_stats() 或通过 mallinfo):
# 用 gdb 或程序内埋点调用 malloc_stats() 打印分配器状态
# 关注 arena 数量、in-use 与 free 的比例
# 更专业的可用 jemalloc 的 profiling、或 valgrind/massif 做堆分析
valgrind --tool=massif ./myapp
# 生成 massif.out.,用 ms_print 查看堆使用曲线
三、物理碎片 vs 虚拟碎片对比
| 维度 | 物理内存碎片 | 虚拟内存碎片 |
|---|---|---|
| 发生层级 | 内核页分配/伙伴系统 | 进程地址空间/堆分配器 |
| 表现形式 | 高 order 块缺失,大块分配失败 | 堆里有空洞,大对象分配失败 |
| 观察文件 | /proc/buddyinfo、/proc/pagetypeinfo |
malloc_stats、heap profiler |
| 常见受害者 | 大页、驱动、虚拟化、DMA | 长跑应用的堆分配 |
| 缓解手段 | 内存规整、内存压缩、透明大页 | 换分配器、对象池、减少频繁分配 |
两者的共同点是:空闲总量够,但凑不出连续的大块。区别在于,物理碎片影响的是内核给谁的连续物理页,虚拟碎片影响的是进程内部的堆布局。
四、内核侧的缓解机制
Linux 内核提供了一些对抗物理碎片的手段,理解它们有助于判断现象和处理方法。
内存规整(Compaction):内核把可移动的页迁移、合并,腾出连续大块。它由 vm.compaction_proactiveness(新内核)和内存水位触发。透明大页(THP,Transparent Huge Pages)就依赖它来提供 2MB 大页。
# 查看透明大页状态
cat /sys/kernel/mm/transparent_hugepage/enabled
# 输出 [always] madvise never
# always:尽量用大页;madvise:只在程序显式请求时用;never:关闭
# 查看规整相关
sysctl vm.compaction_proactiveness 2>/dev/null
sysctl vm.extfrag_threshold
# 手动触发内存规整(谨慎,会消耗 CPU)
echo 1 > /proc/sys/vm/compact_memory
高风险提示:echo 1 > /proc/sys/vm/compact_memory 会触发全系统内存规整,可能造成数秒到数十秒的卡顿,绝对不要在业务高峰期执行。透明大页开关(/sys/kernel/mm/transparent_hugepage/enabled)也会影响延迟和内存占用,对延迟敏感的数据库类应用,很多团队会设为 madvise 甚至 never。改前记录原值,回滚即写回 always 或 madvise 并重启相关服务验证。
# 调整脏页与内存回收参数(改前记录原值)
sysctl vm.dirty_ratio
sysctl vm.dirty_background_ratio
sysctl vm.swappiness
# 数据库服务器常把 swappiness 调低(如 1-10),减少换页,但要结合内存余量判断
内存压缩(zswap/zram):把不常用页压缩后放内存,缓解物理紧张,但会消耗 CPU,是权衡。
大页(HugePages):提前预留大页,避免运行时因碎片分配不到。数据库(如 Oracle、部分版本的 MySQL 配置)常预分配大页。
# 查看大页配置
cat /proc/meminfo | grep -i huge
# 设置预留大页数量(示例,需评估)
sysctl vm.nr_hugepages
# 临时设置
sysctl -w vm.nr_hugepages=1024
五、进程侧碎片怎么办
内核物理碎片多半靠规整和重启缓解,进程堆碎片则更多靠代码和配置优化。
手段一:换更抗碎片的分配器。glibc 的 ptmalloc 在多线程下容易产生较多 arena 和碎片,很多高性能程序改用 jemalloc 或 tcmalloc。这些分配器分级管理、对多线程更友好,碎片和性能通常更好。
# 通过 LD_PRELOAD 使用 jemalloc 而不改代码
# 安装:RHEL/CentOS/Rocky: dnf install -y jemalloc
# Debian/Ubuntu: apt install -y libjemalloc2
LD_PRELOAD=/usr/lib64/libjemalloc.so.2 ./myapp
# 通过环境变量调节,如调整脏页回收
MALLOC_CONF="dirty_decay_ms:1000,muzzy_decay_ms:1000" LD_PRELOAD=/usr/lib64/libjemalloc.so.2 ./myapp
手段二:减少频繁的小对象分配。用对象池、sync.Pool(Go)、ByteBuf 池(Netty)等复用机制,减少分配释放次数,从源头减少碎片。
手段三:预分配或分段管理。对需要大块连续内存的应用(如共享内存、某些数据库 buffer pool),启动时一次性预分配,而不是运行中动态增长。
手段四:控制生命周期。长跑进程的碎片会累积,定期重启(滚动重启)是一种务实手段,但要注意这是"治标",同时缓解内存泄漏和碎片两类问题。
手段五:监控堆分配。给应用接入堆 profiler(如 jemalloc 的 prof:true、Go 的 pprof、Java 的 NMT),观察分配趋势,及早发现异常增长。
# jemalloc 采样配置示例
MALLOC_CONF="prof:true,prof_prefix:/tmp/jeprof,lg_prof_sample:19" \
LD_PRELOAD=/usr/lib64/libjemalloc.so.2 ./myapp
# 之后用 jeprof 分析生成的堆文件
六、典型现象与对应处置
| 现象 | 可能原因 | 排查动作 | 处置 |
|---|---|---|---|
Cannot allocate memory 但 free 有余量 |
物理碎片缺大块 | cat /proc/buddyinfo |
手动规整或重启 |
| 大页分配失败 | 启动后内存已碎 | 看 /proc/meminfo huge 项 |
开机预留大页、尽早分配 |
| 长跑服务内存缓慢上涨 | 碎片累积或泄漏 | 堆 profiler、多次采样对比 | 优化分配或滚动重启 |
malloc 偶发变慢 |
堆碎片导致分配器扫描变慢 | massif、分配器统计 | 换 jemalloc/tcmalloc |
| OOM 杀进程但 total 内存不高 | 某进程含碎片/泄漏 | dmesg、smaps |
定位进程并优化 |
常见问题
Q:内存碎片会导致 OOM 吗?
会。内核在内存紧张时触发内存回收,如果因碎片无法满足分配,可能触发 OOM killer 杀掉进程。表现为"内存没满但进程被杀",可在 dmesg 里看到 OOM 记录。
Q:free -h 显示 free 很少,一定是碎片吗?
不一定。Linux 会把空闲内存大量用作 page cache,free 显示少是正常的。真正该看的是 available 列和 /proc/buddyinfo,而不是裸的 free 值。
Q:重启能解决碎片问题吗?
能缓解物理碎片和进程堆碎片,因为重启后内存重新分配、布局整理。但它是治标,如果根因是内存泄漏或分配模式不合理,重启后还会重现。要看"多久复发"来判断根因。
Q:透明大页(THP)应该开还是关?
看应用。THP 能减少页表开销、提升某些大内存应用的性能,但对延迟敏感的应用(数据库、Redis)可能带来抖动(大页规整时的停顿),很多团队选择 madvise 模式,让应用自己决定。没有通用答案,要压测。
Q:怎么看某个进程的碎片情况?
进程视角没有 /proc/buddyinfo 那样的统一指标,需要借助堆 profiler(massif、jemalloc profiling、Java 的 NMT)观察。也可以通过 /proc/ 看该进程的匿名映射分布。
总结
内存碎片分两种:内核物理碎片(/proc/buddyinfo 观察,表现为高 order 块缺失)和进程虚拟碎片(堆 profiler 观察,表现为堆空洞)。共同特征是"总量够但凑不出连续大块"。
判断物理碎片最直接的指标是 /proc/buddyinfo 的高 order 列,长期为 0 就要警惕;进程碎片则靠 massif、jemalloc profiling、Java NMT 这类工具。
缓解物理碎片的手段:内存规整、透明大页、预分配大页、控制内存水位。注意 compact_memory 和 THP 改动都可能带来卡顿,改前评估、留好回滚。
缓解进程碎片的手段:换用 jemalloc/tcmalloc、减少频繁分配、用对象池、预分配大块、监控分配趋势。
碎片问题往往和内存泄漏混在一起,用"重启后多久复发"来判断根因;滚动重启是务实的止血手段,但治本要靠调整分配模型或换分配器。
企业QQ咨询




