news 2026/10/22 5:11:15

Linux服务器内存排查全指南:从应急处理到根因定位

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux服务器内存排查全指南:从应急处理到根因定位

1. 救命时刻:内存打满后的第一波操作

服务器内存飙到 100%,这大概是每个运维和后台开发都经历过的心跳加速时刻。页面打不开、接口超时、SSH 敲命令都卡半天,更可怕的是不知道什么时候会触发 OOM Killer,把业务进程直接干掉。

先说一句我自己的经验:排查内存问题,第一要务不是找根源,而是先保住现场、保住业务。内存打满跟 CPU 打满还不一样,CPU 打满机器还能响应,内存打满系统可能连排查命令都跑不动,所以响应顺序非常重要。

我一般按这个节奏来:

  1. 先通过现有的监控平台(云监控、Prometheus + Grafana、Zabbix 都行)确认是不是真的所有内存都被吃光了,还是只是某个指标异常。
  2. 用 SSH 或者带外管理(比如云厂商的 VNC 控制台、物理机的 BMC/IPMI)登录服务器,先敲个free -h和uptime看一眼整体状况。
  3. 不要急着 kill 进程,先用top或ps快照当前内存大户。
  4. 判断这是持续增长型还是突发型,再决定是扩容、清缓存、重启服务还是直接限流。

这四步里最容易被忽略的是第 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是 58G,free只剩 1.3G,看起来内存满了。但有经验的运维会先看available,这是系统真正可用的内存估算值。如果available还有空间,说明系统在压力下还能撑一阵。

再看buff/cache。Linux 内核有个特点,它会尽量用空闲内存来做文件缓存(page cache),这本身是好事,能大幅提升磁盘读写性能。问题是很多新手一看used高就慌了,其实buff/cache那部分在内存不够的时候是可以被内核自动回收的。

判断内存是否“真满”,不能只看free那一列,要看available。如果available也很低,比如只有几百 MB,那才是真正的内存危机。

还有一点要注意Swap。Linux 内存不足时,会把不常用的内存页换到 swap 分区。如果si(swap in)和so(swap 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

这里有几个关键字段:

  • RES(Resident 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 Killer(Out 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 这类有运行时环境的应用,内存分布很复杂,需要更细的工具来看。

我常用的命令是pmap:

pmap -x PID

输出里会列出进程的每一段内存映射,包括哪些是代码段、哪些是堆、哪些是栈、哪些是共享库,最后还有总 RSS 汇总。通过对比不同时间点的快照,可以判断是不是堆内存一直涨不回收,或者是不是某个线程栈越开越多。

看pmap输出时要特别关注anon和heap区块,这两个是进程自己动态分配的内存。如果这两个区块持续增长,说明进程内部有内存泄漏或者缓存机制没有上限。

4.2 统计共享内存里的“重复计数”

进程间共享内存这块非常容易造成误判。假设你有 100 个 worker 进程,每个都加载了同一个 500MB 的共享库(比如 glibc、OpenSSL 这种),在top里看 RES,每个进程都算 500MB,加起来就是 50GB,但实际上物理内存里可能只存了一份。

但也不是所有情况都能这样省。私有匿名内存才是每个进程真正独占的部分,比如进程自己 new 出来的对象、创建的缓冲区。想精确统计,我建议用一个叫smem的工具:

smem -t -k -s rss

smem 会正确区分PSS(Proportional Set Size),把共享内存按比例分摊给每个进程。输出的 PSS 才是接近真实物理内存占用的数值。

4.3 用 /proc 手动查看进程内存细节

有些工具没装的情况下,直接翻/proc也能拿到核心信息。最常用的两个:

cat /proc/PID/status cat /proc/PID/smaps

status里有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 内存时我会分三步走:

  1. 先看jstat -gc PID确认堆内使用情况,判断是否堆内溢出。
  2. 再看jcmd PID GC.heap_info确认堆各区分布。
  3. 如果堆正常但 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 内存,先登录 Redis:

INFO memory

看used_memory、used_memory_human、used_memory_peak这几个关键指标。再用redis-cli --bigkeys扫描是否存在大 key。

如果是大 key 的情况,临时方案是分批删除或者压缩 value。如果是持久化导致的内存波动,尽量调整持久化时间避开业务高峰,或者直接关闭持久化改用主从从节点持久化,减少主库压力。

5.5 日志文件导致的“伪内存泄漏”:page cache 暴涨

有些情况内存看着高,其实并非进程泄露,而是文件读写导致 page cache 爆涨。比如你在服务器上跑了个大文件批处理任务,读了几百 G 的数据,内核把读过的内容都放进 page cache,free -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.swappiness=10

想永久生效就写进/etc/sysctl.conf:

vm.swappiness = 10 vm.overcommit_memory = 0

vm.overcommit_memory控制内核是否允许进程超额申请内存。默认 0 是启发式模式,一般够用;如果业务本身有严重的内存突发需求,可以设为 2,并设置vm.overcommit_ratio来限制超卖比例。但要小心:设为 2 之后,进程申请太大内存可能直接失败,对某些应用不友好。

还有个参数值得留意,vm.min_free_kbytes,设置系统保留的最小空闲内存量。如果内存疯涨的时候不想让系统完全卡死,给这个参数设一个值,比如 51200(MB 级别的 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_bytes+node_memory_Buffers_bytes+node_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_count=262144

7.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:format=b,file=heap.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 -c+M按内存排序查看进程实时占用RES、%MEM
ps -eo pid,rss,cmd --sort=-rss快速列出内存占用最高的进程RSS 排序
vmstat 1 5观察 swap 换入换出、内存余量、系统负载si/so、free、buff/cache
pmap -x PID查看单个进程的完整内存映射anon、heap、堆分配
smem -tk按 PSS 统计进程真实内存占用PSS、USS
/proc/PID/smaps查看进程每个内存区域明细RssAnon、RssFile、RssShmem
dmesg/journalctl -k查看内核日志,确认 OOM 事件OOM Killer 记录
docker stats查看容器资源占用每个容器的内存使用量
jstat -gc PIDJava 应用堆内使用情况各代内存占用
jmap -dump生成 Java 堆快照做离线分析heap dump

8.2 什么情况下用什么工具,别一把梭

这里分享一个我自己的选型逻辑:

  • 初步定位用top或ps:又快又直观,先确定大方向。
  • 排查进程内部用pmap和/proc:看内存是堆分配的、代码段映射的还是共享内存映射的。
  • 排查跨进程共享内存误判用smem:PSS 视角最准确。
  • 排查 Java 内存用jstat/jmap/jcmd:JVM 有自己的一套内存体系,必须专用工具。
  • 排查容器内存用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 650Mi

available 只有 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/mysqld

Java 占了 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:MaxDirectMemorySize=1g -XX:MaxMetaspaceSize=256m,把堆外内存纳入可控范围;再排查代码里 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 服务器内存排查的完整方案,到这里就分享得差不多了。最后再提醒一句:在处理内存问题时,心态上不要想着一口气揪出所有元凶。先让系统活下来,再一步步缩小范围,多积累几个典型场景的处置经验之后,你会发现这类问题一点都不神秘。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 5:11:01

AI视频配音情绪控制实战指南:从声学参数到表演级演绎

1. 项目概述:当AI配音不再“念稿”,而是真正“演戏”最近在做几个跨语言内容本地化项目时,反复被同一个问题卡住:客户反馈AI生成的配音“声音很准,但听不出情绪”。比如一段温馨的家庭对话,AI读出来像银行客…

作者头像 李华
网站建设 2026/10/22 5:11:00

逻辑回归在信用卡欺诈检测中的工程实践:可解释、低延迟、可监控

简介:本资源是一份面向高校学生与机器学习初学者的信用卡欺诈检测实战项目,聚焦逻辑回归算法(Logistic Regression)在金融风控场景中的落地应用,适用于期末大作业、课程设计及毕业设计等实践环节。压缩包共3个文件&…

作者头像 李华
网站建设 2026/10/22 5:10:43

PatchTST实战解析:Patch与通道独立如何提升时间序列预测精度

1. 项目概述:PatchTST到底切开了什么做时间序列预测的人,这几年基本都绕不开 PatchTST 这个词。它是某研究团队提出的一个基于 Transformer 的预测模型,全称很长,但核心就几句话:把一段连续的时间序列切成若干小片段&a…

作者头像 李华
网站建设 2026/10/22 5:10:38

Java开发者的大模型应用开发指南:LangChain4j从入门到生产落地

1. 为什么 Java 开发者需要 LangChain4j1.1 大模型应用开发不是 Python 的专利过去一年多,大模型应用开发的教程、框架、示例代码几乎被 Python 生态垄断。LangChain、LlamaIndex、FastAPI 这一套组合拳打得风生水起,搞得很多 Java 后端开发者心里发慌—…

作者头像 李华
网站建设 2026/10/22 5:10:32

Linux虚拟串口tty层字节变换之谜:0x00丢失、0x0A变形的排查与解决

搞虚拟串口调试的时候,有时候会遇到一种很诡异的现象:你往串口里写了一个字节,比如0x00,从另一端读出来却变了样,要么消失,要么变成了0x0A,要么后面莫名其妙多出一个0x0D。第一次碰见这类问题的…

作者头像 李华
网站建设 2026/10/22 5:10:32

GitHub第40周趋势榜解读:终端工具、RAG框架与构建生态的四大信号

1. 第40周趋势榜的四个意外信号每周刷GitHub趋势榜这件事,我从2023年坚持到现在,中间断过几次,但最近半年基本是雷打不动。第40周这份榜单我前后翻了三遍,越看越觉得有意思——不是因为出现了什么惊天动地的新项目,而是…

作者头像 李华