资讯动态

JVM内存排查利器jmap:从堆转储到OOM定位全攻略

发布时间:2026/10/22 18:50:55 来源:尧图企业网站定制
我先把话说在前面如果你是做Java服务端或者中间件维护的jmap这个名字你一定不陌生。但真正到了线上出问题的那一刻很多人脑子里能想起来的只有一条“jmap -dump:formatb,filexxx.hprof ”导出来一个几百MB甚至几GB的文件然后盯着文件发呆不知道下一步该干嘛。这篇文章不打算炒冷饭而是把jmap拆开揉碎讲清楚它在“查内存问题”这件事里到底是什么角色同一条命令在不同场景下该怎么用以及我在实际排查中遇到的那些不写进官方文档的坑。先说清楚jmap能干什么简而言之三件事第一查看整个堆的配置和实时使用情况第二按类统计堆里的对象数量和占用空间第三把当前堆内存完整导出成一个映像文件也就是堆转储heap dump。这三个能力对应完全不同的排查阶段很多人只记住了第三个却忽略了前两个在排查效率上的价值。这篇文章适合正在学JVM调优的开发者、被线上OOM折磨过的运维、以及所有需要在出问题时“留证据”的人。1. 使用jmap之前先搞清楚你要解决哪一类问题1.1 jmap到底是一个什么性质的命令jmap不是调优工具它更像一个法医工具。它不会帮你自动定位到某一行代码有问题它做的是把JVM进程在某一瞬间的内存状态拍下来交给你。理解这一点非常重要因为它的所有选项都是为了回答“内存里到底发生了什么”这个问题的而不是为了直接给出结论。我见过不少人拿着jmap的dump结果去问别人“这代表什么”这其实是用错了工具。jmap导出的是一个数据文件真正的分析要借助其他工具来完成。但这不妨碍jmap成为整个排查链路上最关键的一环——没有它后面的一切分析都是巧妇难为无米之炊。1.2 三个常见排查场景对应jmap的不同用法场景一老年代持续增长YGC频率越来越快。这时候你需要的不是dump整个堆而是先用jmap -heap pid看一眼当前堆的分区使用率再用jmap -histo pid看看哪类对象占用的空间最多。这两步是轻量级的对线上服务的影响很小却能在几分钟内告诉你“该往哪个方向查”。场景二内存直接飙升到接近OOM服务已经出现严重卡顿。这时候需要的是jmap -dump:formatb,fileheap.hprof pid把整个堆内存完整地导出到磁盘。这个操作会在JVM上产生明显的停顿但对于一个已经快要OOM的进程来说留住现场比保持响应更重要。场景三进程反复OOM崩溃重启之后又恢复。这种时候你不能等崩了再想办法而是要在JVM参数里加上-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/data/dump让JVM在OOM发生前自动生成堆转储文件。你甚至不需要手动执行jmap。我每次接到线上内存问题的工单第一步都是先确认是上面哪一种场景再决定要不要跑jmap、跑哪一个选项。盲目地dump整个堆既慢又容易让服务彻底瘫掉。2. jmap三个主力选项-heap看总览-histo圈重点-dump留铁证2.1 -heap给整个堆拍一张配置和使用率的快照jmap -heap pid输出的是JVM堆的配置参数和当前各区域的使用情况。这里面有几个我每次都会重点关注的字段。第一部分是Heap Configuration也就是堆的初始化参数比如MaxHeapSize最大堆、NewSize新生代初始大小、MaxNewSize新生代最大大小、NewRatio老年代和新生代的比例、SurvivorRatioEden区和Survivor区的比例。NewRatio默认是2意味着老年代占整个堆的2/3新生代占1/3。这个比例对吞吐量和GC频率都有直接影响如果老年代频繁打满而新生代还有很大的余量那说明业务对象的生命周期偏长参数可能需要调整。第二部分是Heap Usage显示Eden区、两个Survivor区、老年代各自当前用了多少、容量是多少、使用率百分比。这里有个容易误判的地方Eden区使用率高并不一定是坏事因为每轮Minor GC之后Eden区会清空反而老年代的使用率一旦超过70%甚至80%就需要警惕了说明对象大量进入老年代可能是有对象长期持有了不该持有的引用。jmap -heap输出的其实是JVM某个时刻的瞬时状态仅仅看一次没有太大意义。我在实际使用中的习惯是用脚本每隔十分钟采集一次把输出里的关键数字老年代使用率、元空间使用率存下来对比两三个时间点的变化趋势。如果老年代使用率线性上升就算现在还不到报警线也已经能预见几小时后必然触发Full GC。2.2 -histo从类的维度揪出数量可疑的对象jmap -histo pid输出一张表格按对象数量或者占用字节数排序。默认是按实例个数排序但实际排查时我更常关注字节数那一列。命令格式通常是jmap -histo pid | head -30只看前几十行就够了因为真正的内存大头几乎都排在最前面。表格里的类名有几种很常见的缩写形式稍不注意就会看懵类名含义[Bbyte数组[Cchar数组[Iint数组[Ljava.lang.Object;Object对象数组java.lang.String字符串对象本身拿到这张表之后怎么判断我有一套自己的经验规则。如果排在最前面的全是byte[]和char[]先看它们的数量和单位大小是否合理很多情况下字符串处理类的场景就是会大量产生这种数据不一定有问题。如果某个业务类、连接池类、缓存类对象实例数量高达几十万个每个实例占几千字节那几乎可以确定它们是被意外地长期持有。比如一个自定义的缓存Map没有设置过期时间里面的Entry对象就会越堆越多。jmap -histo还有一个容易被忽略的版本就是jmap -histo:live pid。带:live的选项会先触发一次Full GC再统计存活对象。这在本地环境验证“哪些对象真正存活”的时候很有用但在线上环境请慎重——一次Full GC带来的停顿对高流量服务来说可能直接导致大量请求超时。我的建议是线上分析优先用不带live的版本配合分析工具里的过滤功能排除不可达对象。2.3 -dump:formatb把整个堆完整复制一份留底jmap -dump:formatb,file/data/dump/heap.hprof pid是大家最熟悉的写法这里有两个参数务必记住。formatb表示输出二进制格式的hprof文件这也是后续MAT、jhat等工具唯一能识别的标准格式如果漏了这个参数在某些JDK版本上会输出文本格式文件体积巨大且无法直接用MAT分析等同于白导了一次。file指定导出路径这个看似不起眼的参数在线上却经常把人坑了。首先是路径所在磁盘的剩余空间。一个4GB堆的进程dump出来的文件通常在2GB到4GB之间具体取决于堆里实际存活对象的密度。如果磁盘空间不够导出到一半就会失败而且不会留下任何可用的部分文件。其次是路径的目录权限JVM进程如果是用普通用户启动的dump到某个root-only的目录会直接报Permission denied。我在生产环境会提前创建一个专门的dump目录权限设置好磁盘空间用独立挂载盘避免和日志盘抢空间。dump命令执行之后JVM需要把所有堆内存冻结住再写入磁盘这个过程称为Stop-The-World。堆越大冻结时间越长。一个8GB的堆导出过程可能让服务卡顿几十秒到几分钟不等具体取决于磁盘写入速度。这一点我放在后面专门讲因为它直接决定了你该在什么时机执行dump。新版JDK其实已经逐渐在弱化jmap的地位JDK 8开始官方推荐的替代工具是jcmd命令对应是jcmd pid GC.heap_dump /data/dump/heap.hprof。jcmd的优势是它不需要区分工具版本和进程版本的匹配问题而且以后新特性基本都往jcmd上放。但老JDK环境依然到处是jmap该用还得用。3. 拿到hprof文件之后从jhat初查到MAT找到真正持有者3.1 老版本JDK的jhat与新版本JDK的替代思路很多人dump完之后就懵了文件有了但怎么打开最原始的工具是JDK自带的jhat命令格式是jhat heap.hprof启动之后会监听本机7000端口浏览器访问http://localhost:7000就能看到一堆HTML页面。jhat本质上是把一个巨大的hprof文件转换为可浏览的对象直方图它可以告诉你哪些对象实例数量最多、哪些类占用空间最大。但是它存在两个明显问题第一从未针对大文件做过性能优化几百MB的hprof加载起来非常吃力几个GB的文件基本没法用第二从JDK 9开始jhat已经被官方移除只有JDK 8及以下版本还自带。如果你不得不先用命令行工具做快速验证我建议用jhsdb jmap --heap --pid pid来看堆概览或者直接把hprof文件拖到分析工具里。现在主流的分析工具是Eclipse MAT它也是我在实际项目里用得最多的。面对几个GB的堆转储MAT能在可控时间内完成解析并且提供完整的排查路径。3.2 MAT的核心三件套直方图、支配树、泄漏报告MAT打开hprof文件之后有几个固定的入口Histogram、Dominator Tree、Leak Suspects Report。这三样东西解决不同层面的问题但真正的排查顺序通常是从直方图开始。直方图就是类级别的对象统计跟jmap -histo的表格类似但更加灵活——你可以按类名过滤可以对某个类右键选择“List objects - with outgoing references”查看它引用到了谁。这步操作平时很少有人讲但恰恰是最关键的一步它能告诉你某个对象到底是被谁持有以及它持有了哪些对象。比如你看到有个Hashmap$Entry实例数高达几百万点开它的outgoing引用就能看到这些Entry的key和value分别是什么类型从而判断这到底是一个正常的业务缓存还是无脑往Map里塞数据导致的泄漏。Dominator Tree是MAT最核心的视图中文叫支配树。它把堆里的对象按照“谁支配谁”的关系组织成一棵树。一个对象A支配对象B意思是所有从GC Roots到B的路径都必须经过A换句话说A是B的“顶头上司”只要A活着B就一定会被连带持有。在实际排查中支配树里最顶层的几个节点往往就是内存泄漏的真凶——一个巨大的HashMap、一个静态集合、一个ThreadLocal相关的对象它们自身看起来不大但下面挂着成片的对象树。顺着支配树一路往下看你就能精准定位到那个不该长期存活的“根持有者”。Leak Suspects Report是MAT的自动分析结果它会基于支配树自动找出几个最可疑的“大对象持有者”并给出简要说明。这个东西适合作为排查的起点但不能全信因为它只是基于支配关系做启发式猜测有时指向的并不是真正的元凶。我的做法是先看泄漏报告圈定三五条线索再用支配树验证和深挖最后用直方图反查业务类定位到具体代码。3.3 一次完整实战某订单服务的OOM排查链路说一个我实际经手过的场景。某服务运行一段时间后老年代持续上涨业务侧表现为每隔半小时就出现一次长时间卡顿日志里大量GC超时告警。当时我做了一个非常简单的三步操作第一步jmap -histo pid | head -40发现排在前面的是大量自定义OrderEntity对象和一个自定义缓存类CacheWrapper$Node。这两个类在系统里本不应该同时出现这么多实例。OrderEntity的实例总数有80多万这个数字放在一个日订单量几万的服务里完全不合理几乎可以断定是某种缓存或者队列里的对象堆积了。第二步jmap -dump:formatb,file/data/dump/order-heap.hprof pid把现场完整导出来。这时候服务已经比较不稳定了但还不到OOM的边缘所以我挑了一个业务低峰期在凌晨执行。导出用了大约三分钟文件将近6GB。第三步用MAT打开这个文件。进入支配树之后第一层就看到了那个CacheWrapper$Node它下面挂了40多万个OrderEntity。再对其中一个Node右键查看GC Roots路径发现它被一个静态Map持有而这个Map的初始化代码是在某个“全局缓存管理器”里。到了这一步问题基本就清楚了缓存管理器把每次订单操作的结果都放进了静态Map但没有设置过期时间也没有做数量上限控制。后续修复很简单给Map加上淘汰策略就行但如果没有dump文件靠猜代码这个问题的定位至少要多花一周时间。4. 线上执行jmap必须绕开的四个坑先停一停再动手4.1 大堆dump造成的停顿不只是“慢一下”的问题很多人对jmap -dump的认知是“导个文件嘛最多卡一两秒”。这个认知在堆比较小的场景下勉强成立但到了生产环境的大堆场景就是事故的开端。JVM在导出堆时需要把整个堆的内存状态冻结然后复制出去。这里的关键不是“导出数据量”而是需要变量在某一瞬间保持一致——它要把所有线程暂停确保堆里的对象引用关系不发生变化然后遍历整个堆把对象写入文件。堆越大遍历和序列化越久磁盘写入越慢整个STW时间越拉越长。一个24GB堆的服务导一次文件可能就是上百秒这在线上是不可接受的。我在执行这类操作之前一定会先评估三件事当前堆有多大、服务对停顿的容忍度是多少、有没有业务低峰窗口可以用。如果确实没有低峰期我会把jmap -histo和jstat的趋势数据作为主要分析依据先不动dump等OOM自动触发或者找到能短时摘流量的窗口再导。千万不要在白天高峰期为了“留证据”把核心服务直接STW到秒级。4.2 :live并不是救星线上慎用Full GC触发刚接触jmap的人很容易把-histo:live和-dump:live当成“更精准”的选项毕竟只统计存活对象看起来更干净。但这两个选项背后的实现机制是执行一次Full GC之后再统计或导出。这就有两个问题。第一Full GC本身是线上大忌它会触发完整的Stop-The-World比dump的冻结时间可能更长尤其老年代碎片化严重的时候一次Full GC可能就拉垮整个服务。第二Full GC之后再dump你丢掉了一个非常重要的信息——不可达对象。很多内存问题恰恰需要看那些“即将被回收但还没被回收”的对象长什么样。你在Full GC之后看到的是“干净”的真实存活对象但排查问题的现场已经被你亲手破坏了。我的习惯是线上分析一律不带live。如果觉得不可达对象干扰判断用MAT打开文件之后可以通过过滤条件单独查看Unreachable Objects把两者分开来看。4.3 把出口放对位置磁盘目录与文件命名这条是老生常谈但每次复盘线上dump失败案例都躲不开这个坑。有一次某服务的dump导到一半失败了原因是根目录磁盘余量不足而当时日志文件的盘还有很大的富余。原因很简单jmap的file参数直接写了/tmp/heap.hprof根目录满了自然写不进去。所以在生产环境我会在系统初始化的时候就规划好一个独立的dump目录比如/data/dump挂独立的磁盘分区目录权限与JVM进程运行用户保持一致。文件命名也值得讲究。我见过无数名为heap.hprof或者dump.hprof的文件被反复覆盖到最后都不知道是哪一天、哪个进程导出的。建议统一成主机名-应用名-pid-日期时间.hprof这种格式比如prod-order-03-12345-202502121030.hprof。配合清理策略比如保留5天的dump文件自动删除既不会把磁盘撑爆也能在后续排查时按时间线快速找到对应的现场。4.4 OOM之前自动留证让JVM自己生成堆转储手动执行jmap有一个天然的劣势你人不在现场或者你发现服务出问题的时候已经晚了进程都崩了。更稳妥的做法是提前给JVM打好预防针在启动参数里加上两行-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/data/dump这样JVM在抛出OutOfMemoryError之前会先自动生成一份堆转储文件。这个机制的价值在于你不需要任何人在线操作不需要现场实时介入就能保住崩溃前那一瞬间的内存证据。文件默认生成在启动目录所以最好显式指定HeapDumpPath固定到一个磁盘空间充足的位置。这里还有个容易被忽略的细节OOM发生时堆里可能已经塞满了对象生成的hprof文件往往比平时大得多可能是最大堆的好几倍。如果HeapDumpPath指向的磁盘不足JVM可能因为无法写文件而直接把进程干掉导致连崩溃前最后的现场都丢了。所以HeapDumpPath的位置一定要放在独立分区并且有容灾空间。5. 把jmap固化进日常排查流程一条能直接复制的证据链5.1 手里备一套顺手的脚本关键时刻不慌线上排查最忌讳临时翻命令文档手忙脚乱。我会在每台机器上放一个简单的脚本包含三个命令的组合按需调用。#!/bin/bash # jmaptools.sh pid [action] PID$1 ACTION${2:-status} TS$(date %Y%m%d%H%M%S) DUMP_DIR${DUMP_DIR:-/data/dump} mkdir -p $DUMP_DIR case $ACTION in status) jmap -heap $PID ;; histo) jmap -histo $PID | head -50 ;; dump) FREE$(df -P $DUMP_DIR | awk NR2 {print $4}) if [ $FREE -lt 1048576 ]; then echo dump dir free space too low exit 1 fi jmap -dump:formatb,file$DUMP_DIR/heap-$PID-$TS.hprof $PID echo dump saved to $DUMP_DIR/heap-$PID-$TS.hprof ;; *) echo usage: jmaptools.sh pid [status|histo|dump] ;; esac脚本本身很简单核心价值是统一了三个命令的调用方式并且在一开始就检查了磁盘剩余空间。你可以在没有压力的时候先把这套脚本放好真到出问题的时候省去敲命令和查空间的环节。5.2 证据链思维jmap只是整条链路的起点我现在每次排查前端服务的线上问题时都会刻意培养一种“证据链”的思维方式。拉取的第一个证据是jstat -gcutil的历史数据用来判断GC频率和代际使用率的趋势第二个证据是jmap -histo的当前快照用来快速圈定嫌疑对象如果趋势和直方图都指向某个可疑对象再决定是否导出完整dump。每一步都在回答不同的问题也都在为下一步铺路。这样做还有一个好处万一后续需要复盘整套证据都能按照时间线还原出当时的判断逻辑。不是拿到一个hprof文件就往里狂点鼠标而是每一步都清楚“我在找什么、为什么找、找到了意味着什么”。我在实际排查中还有个个人习惯就是每次执行jmap -dump之前都会顺手先跑一次jmap -histo哪怕这两者隔了一分钟。因为dump出来的文件里包含了整个堆的对象而直方图恰好告诉你从哪个类入口去切入最有价值。没有这个前置信息直接拿6GB文件去MAT里浏览十分低效。5.3 一个值得养成的习惯先定位再动手最后说点个人经验。jmap不是万能的它解决的是“内存现场保存”和“粗略定位”的问题真正的根因经常要结合源代码分析才能落地。但无数案例证明存好dump文件的人赢在了起跑线上——至少他还有分析的机会。线上问题最可怕的状态不是“有问题”而是“问题复现不了、现场也丢了”。我建议你从今天开始就把jmap的基本功练到肌肉记忆拿到PID先jmap -heap看配置和整体水位再jmap -histo看哪类对象占据大头有必要再jmap -dump留现场。这三个动作之间不需要思考太久但能够帮助你在下一次线上事故里比身边所有人更快地锁定方向。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑