Linux内存去哪了?free命令与进程占用排查实战 1. 内存都去哪了一个让运维人半夜惊醒的经典问题你有没有遇到过这种场景登录一台Linux服务器随手敲一个free -h发现可用物理内存只剩几百兆甚至几十兆第一反应是“哪个进程在吃内存”于是ps aux --sort-%mem、top、htop轮番上阵结果翻了三遍进程列表最大的进程也就占了几百兆加起来离总内存还差得远。再回头看free的输出used那一栏高得离谱但进程列表里就是找不到对应的“凶手”。这不是灵异事件也不是系统在骗你。这是Linux内存管理机制里最容易被误解的一个角落几乎每一个刚接触Linux运维的人都会在这里栽一次跟头。我自己第一次遇到这个问题的时候在一台2GB内存的测试机上折腾了整整一个下午重启了三次服务最后才发现问题根本不在进程身上。这篇文章就是把这个坑彻底讲清楚。我会从free命令的输出到底该怎么读讲起然后逐一拆解那些“看不见的内存”到底藏在什么地方再给出完整的排查路径和实操命令。无论你是刚入门的运维新手还是已经工作几年但一直没深究过内存机制的后端开发看完之后应该都能建立起一套完整的排查思路。核心关键词就三个free命令、可用内存、进程占用围绕它们把Linux内存的账本算明白。2. 先搞懂free到底在说什么输出字段逐个拆解2.1 free命令的默认输出与真实含义很多人看free只看一个数字就是free那一列觉得这个数字小就是内存不够了。这个理解不能说完全错但确实过于粗糙。先看一个典型的输出$ free -h total used free shared buff/cache available Mem: 7.6G 6.8G 156M 388M 668M 356M Swap: 2.0G 1.2G 800M大部分人看到free只有156Mavailable只有356M立刻就慌了。但请注意这里有两个关键列经常被忽略buff/cache和available。free这一列表示的是完全没有被使用的物理内存。Linux的设计哲学是“空闲的内存就是浪费的内存”所以内核会尽可能地把没用到的内存拿去做磁盘缓存page cache和缓冲区buffer。这些缓存内存随时可以被回收给应用程序使用所以它们虽然不显示在free里但实际上是“可用”的。available这一列才是真正值得关注的数字。它表示的是在不使用Swap的前提下大概还能有多少内存可以分配给新启动的应用。这个数字是内核估算出来的包含了可回收的缓存部分。所以判断内存是否真的紧张应该看available而不是看free。2.2 buff/cache为什么能涨到几个Gbuff/cache这一列合并了两个概念buffers和cached。用free -w可以把它们分开显示$ free -wh total used free shared buffers cache available Mem: 7.6G 6.8G 156M 388M 120M 548M 356M Swap: 2.0G 1.2G 800Mbuffers是块设备读写时的元数据缓存通常比较小几十到几百兆。cache是页缓存也就是文件内容的缓存。当你读取一个大文件、执行一次数据库全表扫描、或者解压一个压缩包内核都会把读过的数据缓存在内存里下次再读就不用走磁盘了。这个缓存会一直增长直到内存快不够用了才会开始回收。所以一台运行了很久的服务器buff/cache涨到总内存的百分之六七十都是正常的。这部分内存不是“被占用了”而是“被借用了”需要的时候可以还回来。2.3 available才是判断内存压力的核心指标我个人的习惯是排查内存问题第一步永远看available。如果available还有总内存的20%以上基本不用太担心。如果available低于10%那就需要认真排查了。如果available已经接近0并且Swap开始大量使用那就是真正的内存危机。这里有一个容易踩的坑在某些老版本的Linux发行版上free命令的输出格式不一样可能没有available这一列。这种情况下可以用/proc/meminfo里的MemAvailable字段来替代$ grep -E MemTotal|MemFree|MemAvailable|Buffers|Cached /proc/meminfo MemTotal: 7985624 kB MemFree: 160324 kB MemAvailable: 365128 kB Buffers: 123456 kB Cached: 561024 kB/proc/meminfo是最权威的数据来源free命令本质上就是从这里面读数据然后格式化输出的。当你对free的输出有疑问时直接看/proc/meminfo永远不会错。3. 进程列表里找不到的“幽灵内存”六大隐藏去向3.1 内核态内存Slab分配器吃掉的份额进程列表ps、top只统计用户态进程占用的内存内核自己用的内存是不计入进程的RSSResident Set Size的。内核态内存的大头是Slab分配器它负责为内核对象分配内存比如文件系统的inode缓存、目录项缓存dentry、网络连接跟踪表等。查看Slab占用$ cat /proc/meminfo | grep -i slab Slab: 892456 kB SReclaimable: 756320 kB SUnreclaim: 136136 kBSReclaimable是可回收的Slab内存SUnreclaim是不可回收的。如果SUnreclaim很大说明内核中有大量不可回收的对象占着内存这时候需要进一步排查是哪个子系统的问题$ sudo slabtop -o -s cslabtop会按占用大小排序列出所有Slab缓存对象。常见的占用大户包括dentry目录项缓存、inode_cacheinode缓存、ext4_inode_cache等。这些缓存正常情况下是可回收的但如果系统中有大量小文件频繁创建删除dentry和inode缓存可能会涨得很快。3.2 共享内存与tmpfs挂在内存上的文件系统tmpfs是一种基于内存的文件系统它占用的内存在free里会算进shared和buff/cache但不会体现在任何进程的RSS里。常见的tmpfs挂载点包括/dev/shm、/run、/tmp某些发行版默认把/tmp挂成tmpfs。查看tmpfs占用$ df -h -t tmpfs Filesystem Size Used Avail Use% Mounted on tmpfs 3.9G 1.2G 2.7G 31% /dev/shm tmpfs 3.9G 8.0M 3.9G 1% /run tmpfs 3.9G 0 3.9G 0% /sys/fs/cgroup如果/dev/shm被某个应用写入了大量数据比如数据库的共享内存段、某些应用的临时文件这部分内存就会“消失”在进程列表之外。用lsof可以查看是谁在往/dev/shm里写东西$ sudo lsof /dev/shm | head -20另外System V共享内存段和POSIX共享内存也会占用内存但不计入进程RSS。查看System V共享内存$ ipcs -m ------ Shared Memory Segments -------- key shmid owner perms bytes nattch status 0x00000000 12345 postgres 600 134217728 8这里的bytes就是共享内存段的大小nattch是附加到该段的进程数。如果看到很大的段但nattch为0说明是残留的共享内存段没有清理可以用ipcrm删掉。3.3 内核线程与驱动模块不显示在普通ps里的消耗普通用户执行ps aux只能看到用户态进程内核线程kernel thread虽然也会显示但它们的RSS通常是0因为它们没有用户态地址空间。不过内核线程在运行过程中会通过kmalloc、vmalloc等接口申请内核内存这些内存同样不会体现在进程列表里。查看内核线程$ ps -ef | grep \[ | head -20 root 1 0 0 某时 ? 00:00:01 systemd root 2 0 0 某时 ? 00:00:00 [kthreadd] root 3 2 0 某时 ? 00:00:00 [rcu_gp] ...方括号括起来的就是内核线程。它们本身不占多少内存但它们触发的内核操作比如网络包处理、文件系统操作会消耗Slab内存。3.4 内存碎片与预留看得见但用不上的内存Linux内核在启动时会预留一部分内存给特定用途比如crashkernel用于kdump崩溃转储、硬件保留区域等。这些内存在free的total里可能都不显示或者显示为已使用。查看预留内存$ dmesg | grep -i reserved [ 0.000000] Memory: 7923456K/8388608K available (12345K kernel code, 2345K rwdata, 4567K rodata, 1234K init, 5678K bss, 465152K reserved)这里的reserved就是被预留的内存。另外如果系统开启了crashkernel会在启动参数里看到类似crashkernel256M的配置这部分内存从系统启动开始就被拿走了永远不会出现在free的可用内存里。3.5 页面缓存与匿名页的回收优先级差异Linux内存回收有一个优先级顺序先回收干净的页面缓存clean page cache然后回收脏页面缓存需要先写回磁盘再回收可回收的Slab最后才考虑换出匿名页进程的堆栈内存到Swap。这意味着当系统内存紧张时页面缓存会被优先回收。但如果你看到available很低而buff/cache也不高说明页面缓存已经被回收得差不多了剩下的都是匿名页和不可回收的内核内存这时候内存压力就真的很大了。查看内存回收情况$ cat /proc/vmstat | grep -E pgsteal|pgscan|pswpout|pswpin pgsteal_kswapd 123456 pgsteal_direct 7890 pgscan_kswapd 234567 pgscan_direct 12345 pswpin 5678 pswpout 9012pgsteal表示回收的页面数pgscan表示扫描的页面数。如果pgscan远大于pgsteal说明扫描了很多页面但回收效率低内存碎片化可能比较严重。pswpin和pswpout表示Swap的换入换出如果这两个数字在持续增长说明系统已经在大量使用Swap了。3.6 容器环境下的内存统计偏差如果你在容器里执行free看到的可能是宿主机而不是容器的内存信息。这是因为容器默认没有独立的/proc/meminfofree读到的还是宿主机的数据。这种情况下free显示的可用内存很少但容器里的进程确实没占多少因为内存是被宿主机上其他容器或宿主进程用掉的。在容器里判断自己的内存限制应该看cgroup的相关文件$ cat /sys/fs/cgroup/memory/memory.limit_in_bytes $ cat /sys/fs/cgroup/memory/memory.usage_in_bytes $ cat /sys/fs/cgroup/memory/memory.statmemory.limit_in_bytes是容器的内存上限memory.usage_in_bytes是当前使用量。这两个数字才是容器视角下的真实内存情况。如果memory.usage_in_bytes接近memory.limit_in_bytes即使free显示宿主机还有很多内存容器也可能因为达到cgroup限制而触发OOM。4. 完整排查路径从free到根因的六步操作4.1 第一步确认available和Swap的真实状态拿到一台内存“异常”的机器第一步永远是看全局$ free -wh $ cat /proc/meminfo | grep -E MemTotal|MemFree|MemAvailable|Buffers|Cached|Slab|SReclaimable|SUnreclaim|Shmem|KernelStack|PageTables把MemAvailable和MemTotal做个对比算出可用比例。同时看Shmem共享内存、KernelStack内核栈、PageTables页表这几个字段。PageTables在内存密集型应用中可能会很大比如运行了大量虚拟机的宿主机页表占用几个G都是有可能的。4.2 第二步用smem和ps_mem做进程内存的精确统计ps aux里的RSS列其实并不准确它会把共享库的内存重复计算到每个进程头上。比如有20个进程都链接了同一个2MB的共享库ps会把这2MB算20次导致总和远超实际使用量。用smem可以看到更准确的PSSProportional Set Size$ sudo smem -t -k -s pss PID User Command Swap USS PSS RSS 1234 mysql /usr/sbin/mysqld 12.0M 456.0M 478.0M 512.0M 5678 java java -Xmx2g ... 8.0M 890.0M 912.0M 980.0M ... ------------------------------------------------------------------------------- 25 3 users 45.0M 2.1G 2.3G 2.8GUSS是进程独占的内存PSS是平摊了共享内存之后的内存RSS是传统的常驻内存。看PSS的总和才是比较接近真实的值。如果没有smem可以用ps_mem这个Python脚本$ sudo ps_mem -s Private Shared RAM used Program 456.0 MiB 56.0 MiB 512.0 MiB mysqld 890.0 MiB 90.0 MiB 980.0 MiB java ... --------------------------------- 2.8 GiB4.3 第三步检查Slab和内核内存的占用$ sudo slabtop -o -s c | head -20 Active / Total Objects (% used) : 1234567 / 1345678 (91.7%) Active / Total Slabs (% used) : 45678 / 45678 (100.0%) Active / Total Caches (% used) : 123 / 145 (84.8%) Active / Total Size (% used) : 789.45M / 892.34M (88.5%) Minimum / Average / Maximum Object : 0.02K / 0.34K / 8.00K OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME 456789 456789 100% 0.19K 21752 21 174016K dentry 345678 345678 100% 0.58K 34567 10 276536K inode_cache ...如果dentry和inode_cache占用很大说明系统中有大量文件操作。这本身不是问题因为这些缓存可以回收。但如果SUnreclaim很大就要看是哪个缓存不可回收$ cat /proc/meminfo | grep SUnreclaim SUnreclaim: 136136 kBSUnreclaim通常不会太大如果超过1GB可能需要检查是否有内核模块泄漏。4.4 第四步排查tmpfs和共享内存$ df -h -t tmpfs $ ipcs -m $ sudo lsof /dev/shm 2/dev/null | head -20 $ ls -lh /dev/shm/如果/dev/shm里有大文件确认是哪个应用创建的。有些应用比如某些数据库、缓存系统会默认使用/dev/shm作为临时存储如果配置不当可能会把内存吃光。4.5 第五步检查cgroup限制和容器内存$ cat /proc/self/cgroup $ cat /sys/fs/cgroup/memory/memory.limit_in_bytes 2/dev/null $ cat /sys/fs/cgroup/memory/memory.usage_in_bytes 2/dev/null $ cat /sys/fs/cgroup/memory/memory.stat 2/dev/null | head -20如果memory.limit_in_bytes是一个有限值不是9223372036854771712这种超大数说明在cgroup限制下运行。memory.stat里的rss、cache、shmem等字段可以告诉你内存具体花在哪里了。4.6 第六步用/proc/meminfo做最终对账把所有字段过一遍做一个简单的加法验证$ awk /MemTotal|MemFree|Buffers|Cached|Slab|Shmem|KernelStack|PageTables|VmallocUsed/ {print} /proc/meminfo一个粗略的对账公式是MemTotal ≈ MemFree Buffers Cached Slab KernelStack PageTables Shmem 匿名页 预留内存如果左边和右边差距很大说明有某个字段没有被考虑到。这时候可以对比/proc/meminfo和free的输出看看是不是free的格式化过程丢失了信息。5. 常见问题速查与避坑指南5.1 为什么重启服务后内存没降下来这是最常见的问题之一。重启了一个占内存的服务free显示的可用内存却没有明显变化。原因通常是服务占用的内存中有一部分是共享内存或页面缓存重启服务只释放了匿名页部分页面缓存还被内核留着。另外如果服务使用了System V共享内存段重启服务不会自动删除这些段需要用ipcrm手动清理。排查方法$ ipcs -m | grep -v ^key | awk $5 0 {print $2} | xargs -I{} ipcrm -m {}这条命令会删除所有nattch为0的共享内存段。执行前确认没有正在运行的服务依赖这些段。5.2 available还有但系统已经开始OOM有时候available显示还有几百兆但系统日志里已经出现了OOM Killer的记录。这是因为available是一个估算值它假设所有可回收的内存都能被成功回收。但在实际回收过程中可能会遇到不可移动的页面、内存碎片等问题导致回收失败。另外如果OOM Killer触发时系统正在申请一个大的连续内存块比如大页即使总可用内存足够也可能因为碎片问题而分配失败。查看OOM记录$ dmesg | grep -i out of memory $ dmesg | grep -i oom-killer $ journalctl -k | grep -i oom5.3 内存泄漏的排查思路如果available在持续下降且排除了缓存增长的因素那很可能是内存泄漏。排查步骤用smem或ps_mem定期记录各进程的PSS观察哪个进程的内存在持续增长。对于疑似泄漏的进程用pmap -x pid查看内存映射详情。如果是Java应用用jmap -heap pid和jstat -gc pid查看堆内存和GC情况。如果是C/C应用可以用valgrind的massif工具做堆分析或者用gperftools的heap profiler。一个实用的监控脚本#!/bin/bash while true; do date /tmp/mem_monitor.log ps -eo pid,comm,rss --sort-rss | head -10 /tmp/mem_monitor.log echo --- /tmp/mem_monitor.log sleep 60 done5.4 速查表症状与对应排查方向症状可能原因排查命令free低但available高页面缓存占用正常现象free -wh看availableavailable低但进程RSS总和不大Slab、tmpfs、共享内存slabtop、df -t tmpfs、ipcs -m容器内free显示宿主机内存容器无独立meminfo看cgroup的memory.limit_in_bytes重启服务内存不释放共享内存段残留ipcs -m、ipcrmavailable有但OOM内存碎片或大页分配失败dmesg内存持续下降内存泄漏smem定期记录、pmap -x5.5 几个容易踩的坑第一个坑是过度依赖free的free列。很多人看到free列数字小就以为内存不够实际上应该看available。这个习惯需要刻意纠正。第二个坑是忽略shared列。shared表示tmpfs和共享内存占用的内存这部分内存不会出现在任何进程的RSS里但确实被占用了。如果shared很大一定要查/dev/shm和ipcs -m。第三个坑是在容器里用宿主机的排查方法。容器里的free、ps、top看到的都可能是宿主机的数据必须通过cgroup文件来判断容器自身的资源使用情况。第四个坑是看到buff/cache大就想手动清理。echo 3 /proc/sys/vm/drop_caches确实可以清理缓存但在生产环境上这么做会导致后续的磁盘IO性能下降因为缓存被清空后所有读操作都要走磁盘。除非是在做性能测试需要可重复的冷启动环境否则不建议手动清理缓存。6. 个人实操体会与几个实用技巧我在实际排查这类问题的过程中慢慢形成了一套自己的习惯。首先我从来不会只看free就下结论一定会同时看/proc/meminfo和slabtop。其次我会在服务器上常备smem和ps_mem这两个工具它们比ps aux靠谱得多。再次对于容器环境我会把cgroup的内存相关文件做成一个快捷脚本一键查看容器的真实内存使用。还有一个很实用的小技巧用watch命令实时观察内存变化比反复手动执行命令高效得多。$ watch -n 2 -d free -wh; echo ---; cat /proc/meminfo | grep -E Slab|SReclaimable|SUnreclaim|Shmem-d参数会高亮显示变化的部分这样一眼就能看出哪个字段在涨。另外如果你怀疑是某个进程在慢慢吃内存可以用这个一行命令持续记录$ while true; do ps -eo pid,comm,rss --sort-rss | head -5 /tmp/top_mem.log; echo $(date) /tmp/top_mem.log; sleep 30; done跑几个小时之后翻日志哪个进程的RSS在持续增长一目了然。最后说一个关于available的细节。available的计算考虑了页面缓存中可回收的部分但它假设系统当前的内存压力不变。如果你突然启动一个需要大量内存的应用内核回收缓存的速度可能跟不上应用申请内存的速度这时候即使available看起来还有不少应用也可能因为申请内存失败而崩溃。所以对于内存敏感的线上服务available的安全阈值不要设得太低我个人建议至少保留总内存的15%到20%作为缓冲。这套排查思路基本上覆盖了我在实际工作中遇到的所有“内存去哪了”的场景。从free的输出开始沿着Slab、tmpfs、共享内存、cgroup这几条线逐一排查最后用/proc/meminfo做对账绝大多数情况下都能找到那个“隐身”的内存消耗者。