
1. 救命时刻内存打满后的第一波操作服务器内存飙到 100%这大概是每个运维和后台开发都经历过的心跳加速时刻。页面打不开、接口超时、SSH 敲命令都卡半天更可怕的是不知道什么时候会触发 OOM Killer把业务进程直接干掉。先说一句我自己的经验排查内存问题第一要务不是找根源而是先保住现场、保住业务。内存打满跟 CPU 打满还不一样CPU 打满机器还能响应内存打满系统可能连排查命令都跑不动所以响应顺序非常重要。我一般按这个节奏来先通过现有的监控平台云监控、Prometheus Grafana、Zabbix 都行确认是不是真的所有内存都被吃光了还是只是某个指标异常。用 SSH 或者带外管理比如云厂商的 VNC 控制台、物理机的 BMC/IPMI登录服务器先敲个free -h和uptime看一眼整体状况。不要急着 kill 进程先用top或ps快照当前内存大户。判断这是持续增长型还是突发型再决定是扩容、清缓存、重启服务还是直接限流。这四步里最容易被忽略的是第 2 步。很多人在内存快满的时候 SSH 已经连不上了这时候一定要走带外通道云服务器就靠 VNC 控制台物理机就靠 IPMI。我遇到过不止一次内存被打满系统假死SSH 握手都完成不了结果用 VNC 上去一看是某个进程把内存吃光了kill完直接救回来。不过想真正定位“为什么满”光靠第一波的救火操作是不够的。下面我把排查思路一层层拆开讲每一步都有具体命令和判断逻辑。2. 快速确认到底是谁把内存吃干净了2.1 先看 free搞懂内存的“分配逻辑”排查内存的第一步永远是free -h。这个命令虽然基础但很多人看结果的时候会误判。# free -h total used free shared buff/cache available Mem: 62Gi 58Gi 1.3Gi 168Mi 2.6Gi 2.3Gi Swap: 4.0Gi 2.1Gi 1.9Gi注意看这行输出。used是 58Gfree只剩 1.3G看起来内存满了。但有经验的运维会先看available这是系统真正可用的内存估算值。如果available还有空间说明系统在压力下还能撑一阵。再看buff/cache。Linux 内核有个特点它会尽量用空闲内存来做文件缓存page cache这本身是好事能大幅提升磁盘读写性能。问题是很多新手一看used高就慌了其实buff/cache那部分在内存不够的时候是可以被内核自动回收的。判断内存是否“真满”不能只看free那一列要看available。如果available也很低比如只有几百 MB那才是真正的内存危机。还有一点要注意Swap。Linux 内存不足时会把不常用的内存页换到 swap 分区。如果siswap in和soswap out频繁跳动说明内存已经很紧张了系统在做大量的内存换入换出这时候性能会急剧下降我见过有机器卡到 load average 飙到 80 以上。2.2 top 的隐藏用法按内存排序free看的是整体接下来要看进程级别的消耗。top -c进来之后按下Shift M或大写M让进程按内存使用率从高到低排序。这时候就能一眼看到谁是内存大户。PID USER PR NI VIRT RES SHR S %CPU %MEM TIME COMMAND 12345 root 20 0 28.5g 24.7g 15668 S 0.3 39.8 12:34.56 java -Xmx16g -Xms16g -jar app.jar这里有几个关键字段RESResident Set Size进程实际驻留在物理内存里的大小这才是真正占内存的数值。VIRT 是虚拟内存Java 动不动就能到几十 G但不代表真的都占了。SHR共享内存多个进程可能共享同一份不完全是这个进程独占的。%MEM进程占物理内存的百分比。还有一个容易被忽略的地方top默认显示的RES是包含共享内存的所以多个进程的 RES 加起来会超过总内存。真要算某个进程独占的内存得看smem或者/proc/PID/status里的RSS Anon部分。2.3 ps 命令的进阶用法top适合交互式观察但如果是脚本排查或者想把结果保存下来分析ps更趁手。我常用这条命令ps -eo pid,ppid,rss,vsz,cmd --sort-rss | head -20-sort-rss按物理内存倒序排列head -20只看前 20 个。输出里 RSS 是以 KB 为单位的如果数字看着别扭可以手动换算成 GB。这条命令有一个优势可以加--forest参数把进程的父子关系画出来排查内存泄漏时能看出是不是某个父进程 fork 出来的一堆子进程把内存吃光的。比如 PHP-FPM 和 Apache 这类的多进程模型每个请求一个进程请求多了内存就哗啦啦地上去了。3. 真相揭露buffer/cache 和 OOM 的黑盒逻辑3.1 为什么 free 显示已用 99%业务还能扛很久很多第一次处理内存告警的读者最容易在这里翻车。服务器内存显示 99% 已用但实际上还能继续运行原因就是buffer/cache在发挥作用。buff/cache可以理解成一个中转站内核把读过的磁盘文件放在内存里下次再读就不用重新走磁盘写数据的时候也先在内存里攒着再统一刷回磁盘。当业务进程真的需要更多内存时内核会把这些缓存回收掉这就是为什么available会比free高出一大截。判断内存紧张程度的科学方法是看available。我给大家一个参考线available高于总内存 20%正常不用紧张available在 10%~20%注意观察趋势available低于 10%需要介入了available只剩几个 G 甚至几百 M红线必须马上处理顺便说一句free -w可以显示更详细的缓存分类包括anon页匿名内存比如堆和file页文件缓存排查时信息更多一些。3.2 OOM Killer为什么伤害的总是无辜进程当系统内存真的到了山穷水尽内核会启动 OOM KillerOut Of Memory Killer机制。它的做法是杀掉一些进程来释放内存选择的依据是oom_score这个值综合考虑了进程的内存占用、运行时间、进程优先级等因素。最坑的是内存占用最大的进程往往不一定是该杀的。比如你在跑一个大 Java 应用它在初始化阶段会申请大量堆内存还没真正用起来正好碰上内存紧张OOM Killer 就可能误杀它。还有数据库这类重要服务一旦被 OOM 干掉了恢复时间可能比内存释放的时间还长。排查时如果发现某个进程挂了但日志里没报错记得去翻一下系统消息dmesg | tail -50或journalctl -k | grep -i oom这里能看到 OOM Killer 的触发记录包括哪个进程被干掉了、当时每个进程占了多少内存这对事后复盘非常有用。我建议重要业务进程的oom_score_adj设置得低一些让内核尽量别杀它。设置方式很简单echo -1000 /proc/PID/oom_score_adj或者写进启动脚本里。但这里要提醒一句不要把每个进程都设成 -1000不然 OOM Killer 就没法工作了系统可能直接卡死。3.3 共享内存和 tmpfs 的隐藏占用排查内存还有一个极其隐蔽的坑tmpfs。tmpfs是一种基于内存的文件系统默认挂在/dev/shm很多应用会用/dev/shm做共享内存通信。如果你的应用或者某个库用了tmpfs但数据量失控它会不声不响地把内存吃掉一大块。检查方法很简单df -h /dev/shm如果/dev/shm的已用空间特别大几十 G 甚至上百 G那你可能找到了真凶。另外/run目录也经常是 tmpfs比如systemd管理的临时文件、docker容器运行时产生的数据都可能放在这。遇到这种情况解决方案取决于业务。如果是自己的应用写/dev/shm过多可以限流或者改用其他存储如果是某个系统服务的问题可能需要调整 tmpfs 的大小限制在/etc/fstab里重新挂载时加size参数。4. 深挖“隐形内存”进程的内存到底花在哪了4.1 用 pmap 看进程内存分布top能看到进程占多少内存但它说不出这内存花在哪儿了。如果是 Java 或 Python 这类有运行时环境的应用内存分布很复杂需要更细的工具来看。我常用的命令是pmappmap -x PID输出里会列出进程的每一段内存映射包括哪些是代码段、哪些是堆、哪些是栈、哪些是共享库最后还有总 RSS 汇总。通过对比不同时间点的快照可以判断是不是堆内存一直涨不回收或者是不是某个线程栈越开越多。看pmap输出时要特别关注anon和heap区块这两个是进程自己动态分配的内存。如果这两个区块持续增长说明进程内部有内存泄漏或者缓存机制没有上限。4.2 统计共享内存里的“重复计数”进程间共享内存这块非常容易造成误判。假设你有 100 个 worker 进程每个都加载了同一个 500MB 的共享库比如 glibc、OpenSSL 这种在top里看 RES每个进程都算 500MB加起来就是 50GB但实际上物理内存里可能只存了一份。但也不是所有情况都能这样省。私有匿名内存才是每个进程真正独占的部分比如进程自己 new 出来的对象、创建的缓冲区。想精确统计我建议用一个叫smem的工具smem -t -k -s rsssmem 会正确区分PSSProportional Set Size把共享内存按比例分摊给每个进程。输出的 PSS 才是接近真实物理内存占用的数值。4.3 用 /proc 手动查看进程内存细节有些工具没装的情况下直接翻/proc也能拿到核心信息。最常用的两个cat /proc/PID/status cat /proc/PID/smapsstatus里有VmRSS、VmSize、RssAnon、RssFile、RssShmem这些字段能快速区分匿名内存和文件映射。smaps更细按每个内存映射区列出详细统计排查时能看到有没有异常的巨型映射。如果你是开发发现自己写的程序内存一直涨用这两个文件再配合pmap基本能定位到是代码哪里分配了内存没释放。5. 典型场景拆解六种常见内存吃满案例5.1 Java 应用堆外内存失控Java 应用的内存是排查重灾区。很多人的第一反应是看-Xmx也就是堆最大值但Java 进程的真实内存占用远不止堆。我用一个实际案例说明某同事调了-Xmx16g但从top看进程 RES 已经到 25G。原因是 Java 应用还有元空间Metaspace、线程栈、直接内存Direct Memory和 JVM 自己分配的一些结构。尤其是用了 Netty 这种依赖 NIO 的框架Direct Memory 是不受-Xmx限制的不受控制地增长就可能把内存吃爆。排查 Java 内存时我会分三步走先看jstat -gc PID确认堆内使用情况判断是否堆内溢出。再看jcmd PID GC.heap_info确认堆各区分布。如果堆正常但 RSS 一直涨重点查 Direct Memory、线程数量、Metaspace。用jmap -heap PID也可以看堆详情。如果直接内存Direct Memory超了通常-XX:MaxDirectMemorySize需要手动设置。经验值是MaxDirectMemorySize与堆大小之和不超过物理内存的一半留有余量。5.2 数据库MySQL/PostgreSQL的 buffer 池配置过高数据库类的内存问题一般比较直接配置文件的 buffer 开太大了比如 MySQL 的innodb_buffer_pool_size、PostgreSQL 的shared_buffers这些参数决定了数据库进程会占用多少内存作为缓存。常见的坑是16G 内存的服务器把innodb_buffer_pool_size设成 12G再加各种 session 级别的内存排序缓冲、连接数一下子就爆了。这类问题的排查要点有两个先算总账把数据库关键内存参数排出来conn数量 × 每个会话的内存消耗加上 buffer 池看看理论值是否已经超出物理内存。再看连接数数据库连接数如果设置得很大每个连接都会分配排序和 join 的 buffer连接数暴涨时内存会线性增长。用SHOW PROCESSLIST;或SHOW STATUS LIKE Threads_connected;可以快速查看连接数。如果是连接数过高导致的短期先限制连接数长期可以做连接池或者在应用层加限流。5.3 PHP-FPM / Apache 多进程模型下子进程暴涨PHP-FPM 的pm.max_children如果不开上限每个请求来了都会创建子进程请求一多子进程数量就指数级增长每个子进程再占个几十 MB 的内存整体内存直接爆掉。我遇到过一个典型事故某站点平时流量不大pm.max_children配置很随意赶上一波活动流量PHP-FPM 疯狂 fork 进程内存直接打满上游的 Nginx 全部超时。处理思路是ps -ef | grep php-fpm | wc -l先看有多少 php-fpm 进程再算一下每个进程的平均内存ps -o rss,cmd -p $(pgrep php-fpm | head -20) | awk {sum$1} END {print sum/NR/1024}得到单进程均值后估算安全上限。假设机器剩 8G 可用内存单进程 40MB那max_children就应该控制在 200 以下最好留出 20% 余量。5.4 Redis 大 key 和持久化时内存波动Redis 内存问题有它自己的特点存在大 key单个值几 MB 甚至几十 MB时数据的读写和备份会非常吃内存。比如执行BGSAVE持久化时Redis 会 fork 出一个子进程fork 时使用的是写时复制Copy-On-Write技术理论上子进程不修改内存就不会复制但如果父进程在持久化过程中大量写数据内存会被复制瞬间可能导致内存翻倍。排查 Redis 内存先登录 RedisINFO memory看used_memory、used_memory_human、used_memory_peak这几个关键指标。再用redis-cli --bigkeys扫描是否存在大 key。如果是大 key 的情况临时方案是分批删除或者压缩 value。如果是持久化导致的内存波动尽量调整持久化时间避开业务高峰或者直接关闭持久化改用主从从节点持久化减少主库压力。5.5 日志文件导致的“伪内存泄漏”page cache 暴涨有些情况内存看着高其实并非进程泄露而是文件读写导致 page cache 爆涨。比如你在服务器上跑了个大文件批处理任务读了几百 G 的数据内核把读过的内容都放进 page cachefree -h就会显示 buff/cache 涨到夸张的地步。这类情况最容易被误判成内存泄漏。判断方法很简单看free输出的available是否充足。如果available还有一大半业务进程又不卡那就不用管内核会在内存紧张时自动回收。但有一种例外某些应用依赖posix_fadvise或madvise主动指定内存管理策略比如数据库和消息队列有自己的一套缓存机制内核的缓存管理策略可能会和它们冲突这时就需要用sysctl调整缓存回收策略后面细讲。5.6 容器场景宿主机内存被某个容器吃掉运行 Docker/K8s 的环境排查内存问题时多一层维度。有时候free -h看宿主机内存满了实际上不是宿主机上的进程直接占的而是容器里的进程。第一步是在宿主机上跑docker stats --no-stream这个命令会列出每个运行中容器的 CPU/内存使用率。如果某个容器占用特别大再进容器里用前面的思路排查。注意一点如果容器没有设置内存上限docker run没加-m一个容器能把宿主机所有内存吃光甚至影响到其他容器和宿主机自身。所以我一直强调所有部署到生产环境的容器必须设置内存上限不仅是为了单个容器的稳定性也是为了宿主机的整体安全。K8s 环境里则是通过 resource limits 来控制在 deployment 的 spec 里给memory字段设个值超出就会触发容器 OOM 被 K8s 杀掉重启。6. 从应急到治本内存问题的五步处理法6.1 第一步紧急释放内存临时止血内存告警不是每次都能从容排查的有时候是真的快挂了必须先让系统喘过气来。除了杀进程有几个快速释放内存的命令值得记住。sync; echo 3 /proc/sys/vm/drop_caches这个命令让内核丢弃 page cache 和 dentry 缓存立竿见影。但注意这只是释放了 buff/cache 部分不是释放进程的匿名内存。另外看看有没有明明没用了但还驻留内存的进程确认后直接kill。遇到那种卡死的进程用kill -9也不心软该杀就杀。6.2 第二步给系统开启合适的 swap很多人觉得 swap 没用直接关掉了。但说实话在内存告警场景有一块 swap 比没有强太多。swap 不会让系统变快但它提供了缓冲时间能让系统在内存不足时撑住不立刻 OOM给你留下排查和干预的窗口。建议给服务器分配与物理内存相同或一半大小的 swap。如果当时的磁盘有空间临时加个 swap 文件不需要重启fallocate -l 8G /swapfile chmod 600 /swapfile mkswap /swapfile swapon /swapfile重启后如果不想用了就swapoff /swapfile并删掉文件。做这个操作时需要确认磁盘 IO 不会成为新的瓶颈。不要在 SSD 都快满的机器上加 swap会让 SSD 寿命降得快。6.3 第三步调整内核参数让内存更抗压有些内核参数能在内存紧张时显著改变系统的行为。最常用的是vm.swappiness它控制内核倾向使用 swap 的程度取值范围 0~100值越大越积极使用 swap。如果你希望系统尽量先用内存而不是 swap可以调低sysctl -w vm.swappiness10想永久生效就写进/etc/sysctl.confvm.swappiness 10 vm.overcommit_memory 0vm.overcommit_memory控制内核是否允许进程超额申请内存。默认 0 是启发式模式一般够用如果业务本身有严重的内存突发需求可以设为 2并设置vm.overcommit_ratio来限制超卖比例。但要小心设为 2 之后进程申请太大内存可能直接失败对某些应用不友好。还有个参数值得留意vm.min_free_kbytes设置系统保留的最小空闲内存量。如果内存疯涨的时候不想让系统完全卡死给这个参数设一个值比如 51200MB 级别的 KB 量内核会尽量保留这部分空闲内存给紧急分配用。6.4 第四步给应用做配额和限制系统级防护只是兜底真正的治本方案还是给每个应用设好内存限制。Java 应用明确设置-Xms和Xmx防止堆大小动态波动。高并发场景下把这两个参数设成一样的程序启动时一次性申请好内存避免运行中频繁扩容。同时设置-XX:MaxMetaspaceSize和-XX:MaxDirectMemorySize。容器所有容器加-m限制K8s 中设置 limits。数据库按物理内存比例调整 buffer 池大小比如 MySQL 的innodb_buffer_pool_size设置为物理内存的 60% 左右较合理再留出给连接数和临时表的内存。Nginx/PHP-FPM限制 worker 进程数和单个请求内存。做配额时最重要的一点是不能只设一个参数就完事要把应用整体的内存模型想清楚堆、堆外、线程、连接、缓存全部算进去。6.5 第五步建立监控和告警体系最后一条治本建议也是最容易被忽略的没有监控的服务器就是裸奔。我见过太多同事在处理内存问题时很被动都是收到告警之后才开始排查。如果当时有监控记录很多时候看一眼曲线就能判断问题类型内存曲线平缓上升后突然断层式下降典型的定时任务周期性释放内存内存呈线性持续上升大概率是内存泄漏内存瞬时爬升通常是流量激增或者有大查询内存居高不下但业务正常优先考虑 page cache 和配置过大推荐的基础监控指标node_memory_MemTotal_bytes、node_memory_MemAvailable_bytes整体可用node_memory_MemFree_bytesnode_memory_Buffers_bytesnode_memory_Cached_bytes区分缓存每个进程的 RSS 采集通过 node_exporter 默认就带可以用来事后回溯有监控曲线之后再做排查和预案就有据可依了不需要靠猜。7. 常见问题速查一次说清排查盲区7.1 为什么 kill 掉高内存进程后free 显示可用内存没涨多少这个现象很迷惑人但原因通常是被 kill 的进程的内存刚释放又被内核拿去做了 page cache 或者分配给其他进程了。尤其在高 IO 场景下内核会趁着内存空出来立即把磁盘缓存填充进去导致free看不出变化。遇到这种情况你用来判断的指标应该是available而不是free。如果available涨上来了说明内存确实释放了只是被缓存占住了不用纠结。7.2 为什么 free 显示内存充足但业务还是报内存不足这种一般是进程地址空间不够不是物理内存不够。64 位系统上出现这种情况比较罕见但 32 位应用或者某些容器环境会碰到Cannot allocate memory。排查方式ulimit -v查看虚拟内存限制如果限制得很小很多应用会申请失败。另外也可能是vm.max_map_count太小进程能创建的虚拟内存映射数不够用以下命令调大sysctl -w vm.max_map_count2621447.3 为什么查看某个进程 RES 很大但pmap里全是一样的共享库这就是前面提到的多进程模型。每个进程都加载了共享库内核为了节省内存会在物理内存中只保留一份库代码所有进程引用同一份。如果你统计方式用的是 RES 直接相加会把这部分重复计算。这时候应该用 PSS 视角也就是smem的输出或者/proc/PID/smaps里Shared_Clean/Shared_Dirty字段对比。7.4 为什么容器里面 free 显示的内存上限和宿主机不一样容器里运行free -h命令看到的是容器自己的视图。如果你没有正确设置容器可能会看到宿主机的全部内存这会影响容器内部应用的判断和分配策略。在 Kubernetes 环境里Pod 的内存视图由 CGroup 控制建议给容器设置 limits 后在容器内用cat /sys/fs/cgroup/memory/memory.limit_in_bytes确认实际上限并让应用比如 JVM 加-XX:MaxRAMPercentage而不是-Xmx基于容器配额而不是宿主机内存来分配。7.5 为什么系统 uptime 很高但内存曲线缓慢上升缓慢上升一般是内存泄漏的典型特征。常见泄漏源包括线程没有正确关闭连接池没有释放日志对象持有引用导致 GC 不掉JVM 元空间无限增长这种问题要抓泄漏进程的堆栈Java 用jmap -dump:formatb,fileheap.bin PID配合分析工具看对象分布Python 用tracemalloc或者objgraph。定位到分配点之后才能修复代码层面的问题。7.6 查了半天都没定位到还能怎么办偶尔会遇到这种情况top 里进程内存都正常但系统总内存就是少了几十 G。这时候的建议是查slab内存cat /proc/meminfo | grep -E Slab|SReclaimable|SUnreclaim如果SUnreclaim很大说明内核里有些不可回收的对象占着内存可能是内核模块或文件系统的问题。查 GPU 显存如果装了 NVIDIA 驱动显存不会被 free 统计但某些场景下显存映射会影响显存而不是系统内存需要注意的是别混淆。查strace跟踪 mmap 调用定位是哪个进程频繁申请内存。直接看/proc/meminfo的每一项特别是Active(anon)、Inactive(anon)、AnonPages、Shmem这几个字段的数量是否对得上。8. 排查工具盘点常用命令与核心用途8.1 每个运维都该背下来的命令清单命令/工具核心用途关键输出free -h查看整体内存概览区分 used/free/buff/cache/availableavailable 是否充足top -cM按内存排序查看进程实时占用RES、%MEMps -eo pid,rss,cmd --sort-rss快速列出内存占用最高的进程RSS 排序vmstat 1 5观察 swap 换入换出、内存余量、系统负载si/so、free、buff/cachepmap -x PID查看单个进程的完整内存映射anon、heap、堆分配smem -tk按 PSS 统计进程真实内存占用PSS、USS/proc/PID/smaps查看进程每个内存区域明细RssAnon、RssFile、RssShmemdmesg/journalctl -k查看内核日志确认 OOM 事件OOM Killer 记录docker stats查看容器资源占用每个容器的内存使用量jstat -gc PIDJava 应用堆内使用情况各代内存占用jmap -dump生成 Java 堆快照做离线分析heap dump8.2 什么情况下用什么工具别一把梭这里分享一个我自己的选型逻辑初步定位用top或ps又快又直观先确定大方向。排查进程内部用pmap和/proc看内存是堆分配的、代码段映射的还是共享内存映射的。排查跨进程共享内存误判用smemPSS 视角最准确。排查 Java 内存用jstat/jmap/jcmdJVM 有自己的一套内存体系必须专用工具。排查容器内存用docker stats或 cgroup 文件容器环境有点特殊宿主机视角和容器视角要分开看。判断系统级内存压力用vmstat和/proc/meminfo看 si/so、pgscan 这些字段能掌握内存回收压力。8.3 写脚本做内存快照比肉眼观察更靠谱人总有忘记的时候所以我建议遇到顽固的内存问题时直接写个脚本做定时快照#!/bin/bash # 每5分钟记录一次系统与进程内存快照 LOGFILE/var/log/mem_snapshot_$(date %Y%m%d).log while true; do echo $(date %Y-%m-%d %H:%M:%S) $LOGFILE free -h $LOGFILE ps -eo pid,rss,vsz,pmem,comm --sort-rss | head -15 $LOGFILE sleep 300 done把脚本放到后台跑上几个小时再去看那些时间点的数据很多平时不会注意的规律就浮现出来了。比如内存每次都在晚上 8 点定时任务跑完开始增长那排查目标就很明确了。9. 一次真实的问题排查记录完整流程为了让前面的理论落地我分享一个实际场景做了脱敏处理某业务服务器 16G 内存某天内存告警到达 95%业务监控显示接口响应时间明显变长。我的排查过程是这样的第一步登服务器free -h 看一眼# free -h total used free shared buff/cache available Mem: 15Gi 13Gi 750Mi 2.1Gi 1.6Gi 650Miavailable 只有 650M这是危急信号系统随时可能 OOM。第二步top 按内存排序找大户# top -c -M PID USER RES %MEM COMMAND 3210 root 6.2G 41.2 java -Xmx4g -Xms4g -jar user-service.jar 5602 root 2.8G 18.7 /usr/bin/python3 /opt/worker/main.py 1124 mysql 1.9G 12.7 /usr/sbin/mysqldJava 占了 6.2G但-Xmx才 4G这明显不对劲Java 的 RES 超过了配置的堆大小大概率是堆外内存出问题了。第三步jstat 看 Java 堆内# jstat -gc 3210 S0C S1C S0U S1U EC EU OC OU MC MU 8192.0 8192.0 1056.0 0.0 65536.0 38912.3 204800.0 124600.2 48640.0 45560.2老年代用了 124G 中的 120G 左右略高但还在范围内堆内没有爆掉。再结合进程 RES 6.2G推断问题大头在堆外。第四步查线程数和 Direct Memory# ps -eLf | grep 3210 | wc -l 378线程数有 378 个比正常高了不少每个线程有独立的栈空间默认 1MB这就占去了近 400MB。再一看应用用的 Netty 框架直接内存没有设置上限结合之前看过有用户上传大文件的逻辑可能是在上传高峰期 Direct Memory 申请了很多块没有及时释放。第五步验证是 leak 还是高峰正常波动我翻了监控平台的内存曲线发现这个 Java 进程的内存是缓慢上升的每次发布版本后会回落就基本断定是堆外内存使用不当导致的内存泄漏。最终处理策略短期kill掉 Java 进程重新发布新版本因为当时已经找到修复代码的入口内存立刻恢复正常。长期给 JVM 启动参数加-XX:MaxDirectMemorySize1g -XX:MaxMetaspaceSize256m把堆外内存纳入可控范围再排查代码里 ByteBuffer 的分配和释放路径修复泄漏点。这条排查链路最关键的一点是不要停在“谁占的内存大”这个层面一定要多问一步“为什么它占这么多”。top只是看到了表象真正的答案往往藏在进程内部的配置和运行状态里。10. 实战心得那些文档里不写但你必须懂的门道排查 Linux 内存问题这么多年有几个真正体会过的东西想分享一下。第一不要迷信 OOM Killer 的“智慧”。它杀进程的选择机制看起来公平但实际场景里经常误杀。我遇到过 Redis 被 OOM 干掉而真正吃内存的是一个临时 Python 脚本。所以重要服务一定要设置oom_score_adj这是你自己的保命符别指望内核自动处理。第二查看内存要区分“业务应该用的”和“系统机制占用的”。内核很勤奋它会把所有剩余内存都利用起来不管是做缓存还是做别的。你看到 used 98%以为马上要出事了其实可能一大半都是 cache真正的可用内存还挺充足。判断标准永远看 available不要只盯着 free。这个观点我说了很多次但真的值得再强调一遍。第三排查速度很重要但日志存档更重要。很多时候你刚登上去系统就卡死了来不及看任何输出。如果提前把监控和日志做完善比如用 Atop 这类工具记录历史内存快照就能在事后通过回放查看当时到底发生了什么。这类档案越全你在内存问题上的处理经验就越值钱。第四进程重启解决不了根本问题。进程重启后内存确实释放了但如果是配置问题、代码泄漏问题、依赖未正常关闭的连接等问题重启只是把问题推迟了过段时间又会卷土重来。曾经某地的小型 JVM 服务时不时内存飙高每人每次都是重启完事反反复复最后发现是某个版本的驱动存在已知内存泄漏。这种只靠重启永远治标不治本。第五注意那个极易被忽略的/tmp目录。有些程序默认把临时文件写到/tmp如果/tmp也是 tmpfs 挂载有些发行版确实如此那大文件写入就等于往内存里写。检查df -h /tmp和mount | grep /tmp就能避免这类乌龙。11. 从内存排查延伸出去的检查项别忽略负载均值与磁盘 IO排查内存问题遇到瓶颈时我很建议跳出“内存”二字想问题。因为很多看起来是内存不足的症状实际上是负载过高、磁盘 IO 变慢间接导致的连锁反应。top里除了内存字段还有load average。如果三行负载值都比 CPU 核数还高说明系统已经是过载状态内存告警只是其中一个表现。这时候即使你把内存清理干净如果负载还在高位系统还是会响应慢。配合排查命令iostat -x 1如果%util接近 100%磁盘 IO 已经接近饱和。内存不足导致的 swap 换页会进一步加重磁盘读写的负担形成恶性循环。这类场景的处理重点不只是释放内存还要考虑降低并发、优化 SQL、给热点数据加 Redis 缓存、或者扩容磁盘和机器。还包括网络连接数堆积。如果 TCP 连接大量积压每个连接都占着内核的内存缓冲也会让内存居高不下。ss -s能看系统当前连接状态汇总ss -tn可以查具体连接。如果ESTAB或TIME_WAIT数量异常说明网络层也要并行排查。排查的全局观是内存、CPU、IO、网络四个维度要联动看不要孤立地只看内存一项。很多所谓的内存问题根源根本不在内存本身。好的关于 Linux 服务器内存排查的完整方案到这里就分享得差不多了。最后再提醒一句在处理内存问题时心态上不要想着一口气揪出所有元凶。先让系统活下来再一步步缩小范围多积累几个典型场景的处置经验之后你会发现这类问题一点都不神秘。