ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

jstat、jmap、jstack实战:用JDK自带工具排查Java线上问题

jstat、jmap、jstack实战:用JDK自带工具排查Java线上问题 排查Java线上问题很多人习惯先开监控平台再拉ARMS、Prometheus可一旦到了容器环境或者只有一台裸机手里只剩JDK本身的时候你第一反应是什么我见过不少同事连jps都不熟更别提jstat、jmap、jstack这三兄弟。其实JDK自带的这几个命令行工具从来都是Java性能排查里最朴素也最扎实的一层——不需要额外安装包不受网络限制只要机器上有JDK就能直接开干。这篇内容就把jstat、jmap、jstack从原理到命令再到实战完整过一遍适合刚接触Java问题排查的新人也适合平时依赖各种监控平台、突然面对裸奔环境卡壳的老手。这三个工具各管一摊jstat看的是JVM运行时的统计信息GC情况、类加载、编译数据都在里面jmap管内存能把堆里的对象分布翻个底朝天也能直接导出堆快照jstack抓线程运行现场所有线程此刻卡在哪一行、锁被谁持有一清二楚。把这三样组合起来等于给一个运行中的JVM做一次心电图CT现场采访式的全面体检。1. 排查问题的三把手术刀jstat、jmap、jstack各自管哪一摊1.1 这三兄弟为什么到现在还不过时JDK里可视化监控工具不少老的有jconsole、jvisualvm后来还有Java Flight Recorder但命令行三件套的地位一直没被撼动。原因很简单它们轻、快、不挑环境。拿jconsole来说走JMX协议做远程连接在生产环境要开端口、配认证还得担心防火墙拦截jvisualvm在JDK 9之后被从默认发行包拆出去了还得单独下载。而jstat、jmap、jstack是实实在在跟着JDK走的本机诊断工具一条命令下去结果直接打到终端没有任何中间链路。这个特性意味着只要你能登录到应用所在的机器就能用它们。Docker容器里能敲命令吗能哪怕容器里只有一个精简的JRE镜像只要java进程在绝大多数情况下这三条命令都能跑。K8s环境里最常用的排查手段之一就是先kubectl exec进Pod再用jstack抓线程栈配合top -Hp看CPU快速定位问题。占资源这三个工具瞬时占用极低jstack抓一次栈也就几十毫秒的事对运行中的服务几乎无感这也是它们能在高负载环境里安全使用的前提。还有一个被忽视的价值它们是无平台依赖的。企业内部监控系统往往只覆盖核心业务应用一些边缘服务、定时任务、数据处理脚本根本没有监控。突然告警了你总不能先花两小时给它配一套监控吧这时候直接登录机器用JDK自带工具排查是最快的路径。1.2 使用前的几个基础认知在动手敲命令之前有几个概念必须先捋清楚否则命令打出一大堆输出你也只能干瞪眼。第一个是PID。三个工具的核心参数都是目标Java进程的进程号直接用jps拿。jps -l能列出当前机器上所有Java进程的完整主类名或JAR包路径比ps -ef | grep java更干净因为它只筛Java进程不会把其他语言的进程捞进来。注意一点jps是依赖进程启动时留下的临时文件来识别进程的如果Java进程被kill -9过或者用某些特殊方式启动jps偶尔会认不齐这时候用ps -ef | grep java兜底就行。第二个是JDK的bin目录路径。正常情况下直接在终端敲jstat就行但如果你的Java是从tar.gz包解压安装的环境变量又没配好终端会用系统的/usr/bin/jstat而这个符号链接可能指向一个旧版本或错误的JDK目录。最稳妥的办法是定位到你实际使用的Java安装目录用全路径执行避免工具版本和运行中的JVM不匹配导致连不上。第三个是权限问题。这点在排查时特别容易踩。用jstat或jmap去连接一个属于其他用户的Java进程时会报sun.jvm.hotspot.debugger.DebuggerException: Cant attach to the process。原因很简单Attach机制需要读取/tmp/hsperfdata_用户名目录下的性能数据文件或者需要通过ptrace系统调用来attach。也就是说排查用的账号通常得和目标进程使用同一个用户或者干脆用sudo -u切到进程所属用户再执行命令。在有容器隔离的环境里这一条尤其重要很多命令连不上的报错都是权限引发的不是工具坏了。2. jstatJVM运行状态的心电图2.1 jstat的核心参数与常规用法jstat全名是JVM Statistics Monitoring Tool核心用途是实时读取JVM内的统计信息。它不会像jmap那样把整个堆打印一遍而是像心电图一样持续输出采样数据特别适合观察趋势。命令格式如下jstat -选项 -t PID [间隔毫秒 [采样次数]]其中-t表示输出从JVM启动到现在的运行秒数做趋势分析时很有用。间隔毫秒和采样次数配合使用比如想让数据每秒输出一次、持续输出10条就可以写jstat -gcutil -t 12345 1000 10最常用的选项有这么几类选项功能适合场景-gcutil各内存区域使用率及GC次数、耗时日常体检、GC问题初查-gc各内存区域的容量与使用量需要看具体容量大小时-class类加载/卸载数量排查类加载泄漏、频繁卸载异常-compilerJIT编译统计观察编译压力-printcompilation最近编译的方法配合compiler做深度排查我平时用得最多的是-gcutil因为它把老年代、新生代、MetaSpace的使用率直接折算成百分比一眼就能看出哪个区域在异常上涨。如果还需要确认涨的是容量还是使用量再用-gc看具体字节数。2.2 一条命令看懂GC情况与类加载跑一条实际命令感受一下输出长什么样jstat -gcutil 28451 1000 5 S0 S1 E O M CCS YGC YGCT FGC FGCT CGC CGCT GCT 11.25 0.00 64.70 45.12 92.33 88.70 8526 98.421 3 1.024 2 0.021 99.466 10.80 0.00 68.42 45.20 92.33 88.70 8526 98.421 3 1.024 2 0.021 99.466 12.31 0.00 72.55 45.28 92.33 88.70 8526 98.421 3 1.024 2 0.021 99.466 12.90 0.00 76.31 45.35 92.33 88.70 8526 98.421 3 1.024 2 0.021 99.466 13.36 0.00 79.57 45.40 92.33 88.70 8526 98.421 3 1.024 2 0.021 99.466逐列拆给你看。S0和S1是幸存区Survivor的使用率注意它显示的是百分比不是容量。E是Eden区使用率O是老年代使用率M是MetaSpace使用率。CCS是压缩类空间使用率JDK 8以后才有通常和MetaSpace一起变化。YGC是Young GC次数YGCT是Young GC累计耗时单位秒。FGC是Full GC次数FGCT是Full GC累计耗时。CGC是并发GC次数一般在CMS或G1下才有数据CGCT是并发GC累计耗时。GCT是GC总耗时。怎么快速判读如果FGC蹭蹭往上涨说明老年代持续被打满几乎可以断定发生了内存泄漏或堆配置不合理。如果YGC频繁到每秒好几次说明Eden区太小或者对象分配速率过高小对象不断被创建加剧了GC压力。还有一个容易看漏的细节GCT里如果YGCT占比非常高说明Minor GC都花在复制存活对象上往往意味着Survivor区放不下存活对象过早晋升到老年代。另外jstat -class也值得记住命令是jstat -class 28451 Loaded Bytes Unloaded Bytes Time 21445 40151.6 441 421.8 4.27Loaded是从启动到现在累计加载的类数量。如果服务运行时间不长这个数却在以肉眼可见的速度增长基本可以把类加载泄漏列为高度怀疑对象——比如频繁发布脚本、反复创建URLClassLoader又没有释放。2.3 jstat输出里的关键信号与判读方法光会看单次输出还不够关键在对比。我的习惯是拉几组不同时间点的数据放一起看比如12个小时前、当前、以及刚启动时。如果老年代使用率从20%慢慢爬到了80%哪怕FGC次数没怎么涨这也是个危险信号——说明有对象正在缓慢堆积最终会触发一次代价高昂的Full GC。值得警惕的三种组合Eden使用率高 YGC极频繁典型的新生代对象分配过快。优先看代码里是否有大数组、批量对象、JSON序列化循环等在热路径上频繁创建临时对象。我自己遇到过最典型的案例是在一个定时任务里循环创建ObjectMapper实例结果YGC从每分钟几次飙到每秒一次。老年代持续攀升 FGC间隔越来越短基本可以判断是内存泄漏。老年代回收后使用率反弹得越快泄漏越严重。MetaSpace使用率持续增长除了动态生成类的框架CGLIB、ASM、Groovy表达式缓存这类常见原因外还要检查是不是有自定义类加载器没有被卸载干净。jstat -class里的Unloaded列如果长时间不动说明泄漏的类连卸载的机会都没有。这里必须提醒一句jstat给出的只是统计指标它告诉你哪里不正常但不会告诉你为什么不正常。比如老年代涨了到底是谁占的这个问题就要交给下一节的主角jmap来回答了。3. jmap把堆内存的家底翻出来3.1 jmap -heap先看堆配置再说别的jmap的全称是Java Memory Map作用是对JVM内存做透视。第一件值得做的事就是用jmap -heap看一眼当前堆的配置和实时使用情况jmap -heap 28451 Attaching to process ID 28451, please wait... Debugger attached successfully. Server compiler detected. JVM version is 25.321-b07 using thread-local object allocation. G1 GC with 8 thread(s) of collectors (concurrent GC mode) Heap Configuration: MinHeapFreeRatio 40 MaxHeapFreeRatio 70 MaxHeapSize 4294967296 (4096.0MB) NewSize 1048576000 (1000.0MB) MaxNewSize 2576351232 (2457.0MB) OldSize 2097152000 (2000.0MB) ... Heap Usage: G1 Heap: regions 4096 capacity 4294967296 (4096.0MB) committed 4294967296 (4096.0MB) used 2502295552 (2387.0MB)这个命令的价值不在于提供新信息而在于确认JVM实际启动参数到底生效了什么。现实里经常出现这种情况-Xmx在启动脚本里写了4G但通过jmap -heap一看MaxHeapSize只有2G说明参数落到了别的JVM上或者被后一个参数覆盖了。排查内存问题不先确认堆配置后面所有的分析都可能是空中楼阁。注意一点jmap -heap的输出在JDK 8时代能显示完整的堆配置和分代详情到了JDK 11以后部分信息被简化有些场景下直接建议改用jcmd GC.heap_info。但如果你还在用JDK 8这个命令仍然是快速确认堆状态的利器。3.2 jmap -histo谁占着内存不掉头jmap -heap告诉我们用了多少jmap -histo则回答都是什么对象。生产环境里最常执行的是带live前缀的版本jmap -histo:live 28451 | head -40输出是对象数量和占用空间的降序排列看完前面二三十行基本就能锁定大头num #instances #bytes class name ---------------------------------------------- 1: 4021897 324892712 [B 2: 892341 120856432 [Ljava.lang.Object; 3: 120456 56002408 java.util.HashMap$Node 4: 894561 43128455 java.lang.String 5: 120233 28745512 com.example.order.OrderInfo ...看到[B排第一不要慌[B是byte数组很多地方都会用到——文件读取、网络IO缓冲、以及绝大部分对象的底层存储。重点看的是业务对象比如com.example.order.OrderInfo有12万个实例、每个占用字节不少加起来占了28MB多但如果订单量本身就有12万这也不算异常。真正的异常是那些你没想到会大量存在的类比如某个工具类的实例竟然是实例数的前五名。histo还有一个隐蔽的用法观察变化趋势。先用jmap -histo抓一次快照记下总实例数和关键类实例数过段时间再抓一次做对比。如果中间没有大规模请求某个业务对象实例数却持续翻倍别犹豫这通常就是泄漏点。不用导堆文件不做复杂分析两步就定位了。提示-histo:live会先触发一次Full GC生产环境使用前务必斟酌。如果只是看对象分布、不想额外增加GC压力建议直接jmap -histo不带live输出的是当前堆里实际存在的对象也能说明问题。3.3 jmap -dump生产环境导堆的正确姿势当histo能确定异常类、但又看不出引用链路时就得靠堆快照说话了。导堆命令的规范写法是jmap -dump:live,formatb,file/data/heapdir/heap_$(date %Y%m%d_%H%M%S).hprof 28451关键参数拆开说。live表示只导出存活对象导出的文件会小很多分析时也干净formatb指定为hprof二进制格式jvisualvm、Eclipse MAT、jhat都能读file是导出路径记得选磁盘空间充足的分区几百MB的堆导出成hprof后可能涨到12GB。导出之后用Eclipse MAT打开最常见的分析路径是找Dominator Tree里的重本对象再右键查看GC Roots引用链。通过引用链能看到这个对象是被谁强引用、从哪段代码创建出来的。这一趟下来内存去哪了、谁拍板留着它基本水落石出。不过这里我得泼一盆冷水生产环境导堆是有代价的。jmap -dump执行时JVM会进入安全点应用线程全面暂停暂停时间取决于堆大小和对象数量几百MB的堆可能要停顿好几秒。导完一次大型堆应用延迟直接白屏几秒是完全可能的。所以规范的操作流程是先评估影响面尽量在低峰期执行导出的命令最好先确认磁盘空间大堆导出后立刻通知相关方必要时准备应急预案。如果只想做轻量排查优先尝试jcmd GC.heap_dump在多数场景下与jmap -dump等价但更推荐、兼容性也更好。4. jstack线程此刻到底卡在哪4.1 抓线程栈的命令与线程状态说明jstack是排查进程活着但没反应这类问题的主力工具。基本用法jstack -l 28451 /tmp/jstack_$(date %Y%m%d_%H%M%S).txt-l参数会输出额外的锁信息——包括线程持有的锁以及等待的锁排查死锁时必备。抓取结果最好重定向到文件因为线程多的JVM可能一次性输出几百行直接看终端很容易刷屏。打开线程栈文件每一段代表一个线程核心结构是这样的http-nio-8080-exec-12 #32 daemon prio5 os_prio0 tid0x00007f3280a2c800 nid0x2e1b runnable [0x00007f32dc4db000] java.lang.Thread.State: RUNNABLE at java.net.SocketInputStream.socketRead0(Native Method) at java.net.SocketInputStream.socketRead(SocketInputStream.java:116) ... thread-pool-3 #21 prio5 os_prio0 tid0x00007f3280301800 nid0x2bcf waiting for monitor entry [0x00007f32e0f39000] java.lang.Thread.State: BLOCKED (on object monitor) at com.example.order.OrderService.createOrder(OrderService.java:108) - waiting to lock 0x000000076d2a5f70 (a com.example.common.Counter) ... thread-pool-4 #22 prio5 os_prio0 tid0x00007f3280302000 nid0x2bd0 waiting on condition [0x00007f32e0e4b000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) ...线程状态是第一步判断依据RUNNABLE正在执行但网络IO等待比如Socket读取也会显示RUNNABLE所以看到RUNNABLE不代表正在消耗CPU还要看栈顶是否是native方法。如果栈顶是socketRead0这类网络方法多半是线程在等网络数据正常得很。WAITING / TIMED_WAITING通常是等锁、等条件变量、等网络响应。大量线程堆在这里并不一定异常可能是线程池排队等任务。BLOCKED (on object monitor)线程想拿一把锁没拿到被卡在同步块门口。如果一堆线程BLOCKED在同一把锁上基本就是锁竞争激烈或者持锁线程卡住了。看栈信息有个讲究别只看栈顶把整个栈从下往上看一遍。栈顶方法往往是无意义的等待点真正触发问题的业务方法在栈的中间。我曾经排查过一个线程WAITING状态的问题栈顶是park看着人畜无害往下一翻才发现它在一个数据库查询的Future.get()上阻塞再往下是业务代码调用位置问题根源瞬间清晰。4.2 定位死锁一次教科书式的线程栈分析死锁是jstack最擅长抓的问题之一甚至用不着手动分析因为jstack在模式匹配到死锁时会直接在文件末尾打印一段总结Found one Java-level deadlock: thread-pool-1: waiting to lock monitor 0x00007f3280882700 (object 0x000000076d2a5f70, a com.example.common.Counter), which is held by thread-pool-2 thread-pool-2: waiting to lock monitor 0x00007f3280882b00 (object 0x000000076d2a5f80, a com.example.common.StatsRecorder), which is held by thread-pool-1 Java-level deadlock detected.这段输出的意思是thread-pool-1持有Counter锁想要StatsRecorder锁thread-pool-2持有StatsRecorder锁想要Counter锁双方各攥着一把不放互相干瞪眼。定位到死锁之后修复方向通常有三一是调整加锁顺序让所有线程都按同一个全局顺序获取锁这是最根本的解法二是用ReentrantLock的tryLock加超时拿不到锁不硬等三是缩小同步块范围只在真正需要保护的临界区加锁。我遇到过一次因为业务代码在持锁期间调用远程接口而导致的死锁把同步块外的远程调用抽出去后问题彻底消失。值得多提一嘴的是jstack打印的锁信息是monitor级别的锁对于java.util.concurrent里的显式锁不会直接显示held by这种描述但末尾的Locked ownable synchronizers段会给出线索Locked ownable synchronizers: - 0x000000076d2a5f70 (a java.util.concurrent.locks.ReentrantLock$NonfairSync)看到这里就知道线程手里还攥着哪些ReentrantLock配合业务代码能推断出锁的持有路径。4.3 用jstack响应CPU占用飙高top -Hp配合法生产环境最让人头痛的场景是应用没崩、接口还在响应但某几个线程把CPU吃得死死的整个服务时延飙升。这时候光抓jstack没用你得先知道谁是高CPU线程再抓它的栈才有意义。标准步骤是这样的。第一步用top -Hp 28451查看进程内每个线程的CPU占用找到CPU接近100%的那一行记下它的线程号PID列这个号是十进制的top - 10:23:45 up 20 days, 2:33, 2 users, load average: 4.70, 5.90, 5.10 Threads: 65 total, 1 running, 64 sleeping, 0 stopped, 0 zombie %Cpu(s): 90.0 us, 5.0 sy, 0.0 ni, 5.0 id, 0.0 wa, 0.0 hi, 0.0 si PID USER PR NI VIRT RES SHR S %CPU %MEM TIME COMMAND 3721 app 20 0 12.345g 2.345g 18m R 99.3 2.9 23:45.51 java第二步把高CPU线程号转成十六进制。转换方式用printfprintf %x\n 3721得到e89。第三步用这个十六进制数在jstack输出里反查线程jstack 28451 | grep -A 30 nid0xe89找到对应的线程栈栈顶如果是一段纯计算代码比如正则表达式回溯、序列化、循环拼字符串那就明确锁定了元凶。这个方法还有个变体适用于容器环境无法用top -Hp的场景# 用 ps 找到线程 CPU 占比高的线程 ps -Lp 28451 -o pid,tid,pcpu,comm | sort -k3 -rn | head -5 PID TID %CPU COMMAND 28451 3721 99.3 java然后同样转十六进制、grep nid。整套链路下来从负责的线程是谁到它正在跑什么代码基本不到一分钟。注意为了减少误判抓jstack前建议连续抓23次每抓一次间隔3秒左右。如果某线程每次都出现在昂贵的计算栈里才叫实锤偶尔出现一次可能只是任务调度抖动。5. 组合实战一次线上服务卡死的完整排查过程5.1 现象与第一反应去年遇到过一个典型的案例一个订单处理服务中午高峰时段突然大面积超时成员反馈接口从几十毫秒涨到十几秒。登录机器后ping正常进程还活着但CPU负载已经拉满load average接近核数的三倍。第一反应是抓高CPU线程用top -Hp定位到两个线程几乎各占99%的CPU。查线程栈后发现这两个线程都卡在同一个函数——一个自定义的字符串解析工具里具体是正则表达式在循环中执行替代操作。事情到这还没结束。按经验一个正规的服务不可能拿正则去做热路径上有大量并发的文本处理但这次只是表象。顺着这两个线程继续看它们的调用栈都指向同一个第三方配置中心客户端的回调逻辑回调在业务线程池中执行每次配置刷新都会触发一轮全量配置解析而解析代码又用了性能极差的正则表达式最终导致所有线程池线程被拖死。5.2 三招连用的排查链路整个过程把三兄弟全用上了链路清晰得可以复盘第一步是jstack抓现场。高峰时段不管三七二十一先抓两轮线程栈间隔5秒。对比两轮栈发现大量线程堆积在同一个业务方法等待队列上而占CPU的两个线程反复出现在同一个解析工具代码里。第二步是验证是不是只有这两个线程在烧CPU。这是关键因为如果只有两个线程烧CPU其他线程应该不受影响但事实是整个线程池的资源被这两个线程占住其他任务全部排队等待响应时间自然全线上涨。说白了不是线程池线程都被耗尽而是CPU时间片被一个热点代码路径垄断了池子里所有任务都在抢那点剩余资源。第三步是jstat验证GC状态。先排除GC噪音干扰跑了一遍jstat -gcutil -t 28451 1000 5确认GC次数和耗时都在正常范围老年代使用率稳定没有 Full GC 频繁发生的迹象。这样一来GC因素被排除问题聚焦在CPU密集代码上。第四步为了排除堆里存在异常积累的大对象顺手用jmap -histo:live导了一次对象分布看了一遍前30行也没有意外发现。到此内存层面被排除方向收敛到业务代码热点。整个排查从现象到定位大概花了20分钟。最后修复很简单把配置解析工具类里的正则替换改为基于字符串拆分遍历的一次性解析并且把配置解析从回调线程中挪到单独的异步任务避免再次拖垮业务线程池。5.3 整个过程的复盘与前车之鉴复盘时能提炼出几条经验单独放出来值得记住先抓线程栈再想对策。遇到服务卡死不要急着重启重启确实能恢复但问题原因就永远黑箱了。先抓栈、确认方向再决定要不要快速止血。CPU高和线程池阻塞是两码事但它们会互相引爆。这个案例里真正致命的是线程池被热点代码占满导致根本接不住新任务。排查CPU问题时永远要问一句这个线程池还有余力处理其他任务吗jstat的价值主要体现在排除法上。它负责排除GC因素把问题范围一步步缩小。很多时候没查出问题本身就是重要的结论——不是每个故障都伴随GC异常别被JVM问题GC问题的惯性带走。工具不能替代代码审查。三件套能帮你快速锁定线程和内存的异常但最终还是要落到业务代码。排查工具的终点往往是打开源码的那一刹那。6. 哪些坑我替你踩过了常用注意事项6.1 attach fail、权限问题与Docker场景用这三个工具最常见的报错之一是Unable to open socket file: target process not responding or HotSpot VM not loaded这个报错有几个原因。最常见的是-XX:DisableAttachMechanism被设置了JVM主动关闭了动态attach能力这种情况下任何Attach工具都连不上只能通过改启动参数或用jcmd带参数的方式规避。第二个常见原因是进程属于其他用户上文提过切到目标进程的属主再执行就行。第三个原因是容器场景JDK的attach机制需要读取/tmp/hsperfdata_user/pid这个文件而容器内的/tmp可能被清理或配合只读文件系统导致文件不存在这时可以用jps -l先确认是否能发现目标进程不行就考虑挂载临时目录或改用jcmd。还有一类诡异的情况jmap -dump执行到一半报Error - sun.jvm.hotspot.debugger.DebuggerException: Can not get field value。常见诱因是JVM版本和工具版本不一致——注意JDK是向下兼容的不是向上兼容。用JDK 17的jmap去连JDK 8的进程可能出现内部数据结构不匹配反过来用JDK 8的jmap去连JDK 17的进程基本必挂。排查机器上多版本JDK并存时一定先java -version确认默认版本再用与运行版本一致的jmap。另外必须重点提醒不要在高并发下的生产环境对jmap -dump掉以轻心。前面说过dump会触发安全点全停顿大堆停顿几秒到几十秒都有可能。我在一次事故里见过有人直接导出8G堆内存和CPU瞬时飙满监控图直接拉成断崖。导堆前先评估业务容忍度优先用-live参数限制导出范围为存活对象以压缩体积能放在低峰期就放在低峰期。6.2 JDK 9的变化与替代工具简述JDK 9开始Java把原来的tools.jar打散为模块很多老脚本开始报错jvisualvm不再打包进默认发行版JConsole能连的协议范围也变了但这三兄弟依然保留在发行包里命令名字没变。只是从JDK 9往后Oracle建议用jcmd替代部分jmap的工作比如jcmd 28451 GC.heap_dump /data/heap/heap.hprof jcmd 28451 GC.class_histogram jcmd 28451 Thread.printjcmd的优势在于交互方式更统一信息展示也更结构化很多jmap -dump、jstack的活它都能干。但老工具并没有被淘汰很多老系统还在跑JDK 8这些命令在那边完全够用而且命令行习惯一旦形成在快节奏的故障响应中不容易产生歧义。如果你拿到的是JDK 11以上的进程jmap -heap的信息展示会有所变化部分字段被拿掉这时候优先用jcmd GC.heap_info。jstack在JDK 11以后也能用但官方更推荐jcmd Thread.print输出基本一致。做决定前记住一条原则能用jcmd就优先jcmd但会读jmap和jstack的输出永远不吃亏因为那套分析思路是通用的。还有个容易被忽略的点从JDK 8u211以后jmap -help里多了一个-j参数允许你传JVM参数给工具进程本身。排查一些疑难问题时比如工具进程启动时默认附加器内存不够通过jmap -J-Xmx4g这种写法能规避偶尔出现的OOM。大多数场景用不上但知道有这手等真遇到时能省不少时间。回到最初的问题为什么老牌命令行工具到今天还是金刚不坏因为它们贴着一行层技术真实运行现场没有中间层的过滤和美化你说它是ROOT也好、是后门也好反正在排查那些说不清楚哪里不对的诡异问题时手里有一把能直接切开JVM的命令行工具永远比只会在监控页面里点鼠标踏实得多。这份踏实其实就是这些老工具留下来的最大价值。
返回列表