一次 Java 进程神秘退出的排查实录:从 120G 内存到 tmp 目录里的“隐形杀手” 摘要本文记录了一次线上 Java 进程反复神秘退出的完整排查过程。服务器拥有 120G 物理内存JVM 堆仅占用 40G 左右且无其他应用但进程仍被 Linux 内核的 OOM Killer 反复击杀。通过逐层排查系统日志、JVM 内存占用、共享内存Shmem与 tmp 目录临时文件的关系最终定位到根因某个进程将 tmp 目录下的临时文件通过 mmap 映射到共享内存文件持续增长导致内存被耗尽。文章还详细解读了 Linux 内核vm.overcommit_memory参数 0、1、2 三种内存分配策略的含义与取舍并结合本次问题给出线上 Java 服务的选型建议与监控改进方案。1. 现象进程又没了那天下午运维群里突然炸了锅线上 Java 服务又挂了。这已经是本周第三次每次都是毫无征兆地消失没有 OOM 日志没有 core dump连系统日志里都找不到明显的报错。更诡异的是服务器配置并不低——120G 物理内存JVM 堆只给了 40G 左右而且这台机器上除了这个 Java 进程再没有跑其他应用。按理说40G 的堆在 120G 的机器上怎么也不该因为内存不够而挂掉。可事实就是进程像被什么东西“掐死”了一样悄无声息地退出了。2. 第一步先确认是不是 JVM 自己崩了排查的第一步自然是先看 JVM 有没有留下什么“遗言”。我登录服务器先翻了翻 JVM 的日志目录看看有没有 hs_err 文件或者 dump 文件ls -l /opt/app/logs/ | grep -E hs_err|core|dump find / -name hs_err_pid* -type f 2/dev/null结果一无所获。没有 hs_err没有 core dump也没有 JVM 自己生成的 dump 文件。这说明 JVM 并不是因为自身崩溃比如致命错误、JIT 编译问题而退出更像是被操作系统“请”出去的。接着我看了下系统日志重点找 OOM Killer 的痕迹dmesg -T | grep -i -E killed process|out of memory|oom grep -i oom /var/log/messages | tail -50日志里确实有 OOM Killer 的记录而且被杀的进程 PID 正是我们的 Java 进程。这就对上了——进程不是自己崩的是被 Linux 内核的 OOM Killer 干掉的。3. 第二步为什么 40G 堆会被 OOM Killer 盯上这里就出现了一个矛盾点JVM 堆才 40G机器有 120G怎么会触发 OOM要解开这个谜得先搞清楚一个关键概念JVM 占用的内存远不止堆那 40G。除了堆还有元空间Metaspace、线程栈、直接内存Direct Memory、JIT 编译产物、GC 相关的数据结构等等。这些统称为 JVM 的“非堆内存”。如果这些部分加起来很大JVM 实际占用的 RSS常驻内存可能远超堆大小。我赶紧用 jcmd 和 ps 确认了一下 JVM 实际占用的内存jcmd pid VM.native_memory summary ps -o pid,rss,vsz,cmd -p pid结果让我有点意外JVM 的 RSS 确实在 40G 左右并没有出现非堆内存暴涨的情况。也就是说JVM 自己并没有“多吃多占”。那 OOM Killer 为什么还要杀它唯一的解释是系统内存真的不够了。可 120G 的机器JVM 才占 40G剩下的 80G 去哪了4. 第三步用 free 和 smem 揪出内存去向我决定先看看系统内存的整体分布free -g cat /proc/meminfo | grep -E MemTotal|MemFree|MemAvailable|Buffers|Cached|Shmem|Slabfree 的输出让我心里一沉内存确实被吃光了。但奇怪的是Buffers 和 Cached 都不高Slab 也正常唯独 Shmem共享内存这一项高得离谱而且还在持续增长。为了进一步确认是谁在占用共享内存我用了 smem 和 ipcssmem -t -k ipcs -mipcs 列出了不少共享内存段但光看这个还不够我得找到这些共享内存段对应的进程。于是又用 lsof 查了一下lsof | grep -i mem | head -50排查到这里方向逐渐清晰系统里有一块共享内存区域占用量持续增长而且从不释放。这块内存的增长和 JVM 的退出时间点高度吻合——每当共享内存涨到某个临界值OOM Killer 就会出手。5. 第四步共享内存和 tmp 目录的“暧昧关系”共享内存不会凭空增长背后一定有某个进程在持续写入。我顺着这个思路开始对比共享内存的增长和系统里其他指标的变化。我注意到服务器上有一个 tmp 目录里面的临时文件数量一直在增加。起初我没太在意以为只是普通日志或临时文件的堆积。但当我用 du 和 lsof 对比之后发现了一个惊人的规律du -sh /tmp/* lsof L1 | grep -i deleted | head -30tmp 目录里临时文件的总大小和共享内存的占用量几乎同步增长两者呈明显的正相关。更关键的是这些临时文件被某个进程打开后文件虽然被标记为“deleted”但句柄一直没有释放——这正是典型的“文件被删除但进程仍持有句柄”的场景。到这里我基本可以断定这些临时文件并不是普通的磁盘文件而是被映射到了内存里。也就是说某个进程把 tmp 目录下的文件通过 mmap 映射到了共享内存区域文件不断增长映射的内存也不断增长而且进程从不主动释放。6. 第五步验证——内存映射的“铁证”为了验证这个猜想我做了两件事。第一查看进程的 maps 文件确认是否有 tmp 目录下的文件被映射进内存grep /tmp /proc/pid/maps | head -20第二用 pmap 看具体的内存映射明细pmap -x pid | grep /tmp | head -20结果证实了我的判断确实有进程把 tmp 目录下的临时文件通过 mmap 映射到了内存中。文件每增长一块映射的共享内存就跟着涨一块而且这些映射的内存被标记为“不可回收”OOM Killer 在计算可用内存时这部分会被算作“已占用”。真相大白了不是 JVM 的问题而是服务器上某个进程很可能是业务代码里某个库或组件在 tmp 目录下不断创建临时文件并通过内存映射的方式持续占用共享内存最终把系统内存耗尽导致 OOM Killer 把无辜的 Java 进程给杀了。7. 深入Linux 对应用内存分配的三种策略0、1、2排查到这里问题本身已经定位清楚了。但作为一个负责任的排查帖我还想多说一句为什么 OOM Killer 会选中我们的 Java 进程这就涉及 Linux 内核的一个关键参数——vm.overcommit_memory。这个参数控制着内核如何对待进程的内存申请一共有三个取值取值策略名称含义0启发式策略Heuristic Overcommit内核根据当前内存使用情况粗略判断是否允许进程的内存申请。这是大多数发行版的默认值。它允许一定程度的内存超卖但会在系统内存明显不足时拒绝申请。这种策略下OOM Killer 的触发时机比较“模糊”容易出现“看起来内存还够但进程还是被杀”的情况。1总是允许Always Overcommit内核从不拒绝进程的内存申请无论系统内存是否充足。这种策略下进程可以申请远超物理内存的虚拟内存但一旦实际访问内存导致物理内存耗尽OOM Killer 就会立刻介入随机或按评分杀掉进程。这种策略适合对内存申请成功率要求极高、且能接受 OOM 风险的场景。2禁止过量Never Overcommit内核严格按照“物理内存 swap”的总量来审批内存申请不允许任何超卖。进程申请的内存如果超过系统可用总量会被直接拒绝返回 ENOMEM。这种策略最保守能最大程度避免 OOM但可能导致一些需要大内存的进程比如 JVM 启动时预留的堆空间直接启动失败。8. 结合本次问题线上到底该用哪种策略回到我们这次的问题。服务器默认的vm.overcommit_memory是 0启发式策略。在这种策略下内核允许一定程度的内存超卖但判断标准比较“模糊”。当共享内存被 mmap 文件占满后系统可用内存急剧下降内核在某个临界点触发了 OOM Killer而它选中的“牺牲品”恰恰是内存占用最大的 Java 进程。那么正常线上应该用哪种策略我的建议是如果业务对内存申请成功率要求极高且能接受 OOM 风险可以设置为 1总是允许。但要注意这种策略下 OOM Killer 的杀伤力更大因为系统可能已经严重超卖一旦触发往往是批量杀进程。如果业务对稳定性要求极高宁可启动失败也不愿意运行中被杀可以设置为 2禁止过量。这种策略最安全但 JVM 这类需要预留大内存的应用启动时可能因为内存申请被拒而直接报错。对于大多数线上 Java 服务我建议保持默认的 0启发式策略但前提是必须做好两件事一是监控好系统内存的“隐性占用”比如共享内存、Slab、不可回收的 mmap二是给 JVM 和系统预留足够的安全余量。本次问题恰恰是栽在了“隐性占用”上——共享内存的增长完全不在常规监控视野内等到发现时系统已经被拖垮了。换句话说策略本身没有绝对的好坏关键是要理解每种策略的取舍并结合自己的业务场景和监控能力来选择。对我们这次的问题而言即使把策略改成 2也只能避免 JVM 被误杀但根本问题——tmp 目录下不断增长的 mmap 文件——不解决内存迟早还是会被耗尽。9. 复盘与总结这次排查从“Java 进程神秘退出”到“共享内存持续增长”再到“tmp 目录临时文件被 mmap 映射”整个过程像剥洋葱一样一层一层揭开真相。复盘下来有几个关键点值得记住JVM 占用的内存远不止堆那点。排查内存问题时一定要看 RSS 和 Native Memory而不是只看堆大小。系统内存的“隐性占用”是最大的坑。共享内存、Slab、不可回收的 mmap这些都不在常规监控里但它们可能悄悄吃掉几十 G 内存。OOM Killer 杀进程不代表进程有问题。它只是“按内存占用评分”选了个最大的很多时候是替别人背锅。mmap 临时文件要警惕。业务代码里如果用了内存映射文件一定要确保文件有上限、映射有释放否则就是一颗定时炸弹。最后我把这次的经验沉淀成了一条监控规则除了常规的 CPU、内存、磁盘一定要把/proc/meminfo里的 Shmem 和 Slab 纳入监控一旦发现持续增长且不回落就要立刻排查别等到 OOM Killer 替你“报警”。