ARTICLE DETAIL

资讯详情

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

JDK自带三件套jstat+jmap+jstack实战:JVM性能排查与内存分析指南

JDK自带三件套jstat+jmap+jstack实战:JVM性能排查与内存分析指南 1. 工具速览JDK自带监控三件套到底能干什么排查Java生产环境问题很多人第一反应是上VisualVM、Arthas这些重武器。但很多时候机器上根本没装这些工具尤其是客户机房、容器环境网络隔离加上权限管控想装个Arthas比登天还难。这时候JDK自带的jstat、jmap、jstack就成了救命稻草——只要有JDK就有这三个工具不需要额外安装不需要网络下载直接就能用。这三个工具各自盯一个方向jstat看JVM的运行状态重点是GC情况和类加载情况jmap看JVM内存里的对象分布负责导出堆快照或者查看堆统计jstack看线程状态排查死锁、线程卡死、高CPU撑到爆这类问题。配合起来用就是一套完整的JVM体检三件套。适合谁来读后端开发、运维、SRE、测试工程师都能用得上。尤其是那种本机复现不了、只能在生产环境临时排查的场景掌握这三个工具比什么花哨的监控平台都实在。需要注意的是这三个工具对JDK版本有依赖。JDK 8及之前jmap、jstack、jstat都在$JAVA_HOME/bin目录下直接能用。JDK 9到JDK 11之间jmap和jstack被标记为待移除但还能用。到了JDK 12之后Oracle JDK里这两个命令被正式移除了但OpenJDK里很多发行版还保留着比如Adoptium、Amazon Corretto这些。至于最新的JDK 17、JDK 21建议优先用jcmd替代这个话题我后面会专门说。先说清楚如果你的生产环境是JDK 8那这三个工具就是标配放心用。2. jstat实测指南看GC和类加载状态2.1 jstat的定位与核心参数速记jstat全称是JVM Statistics Monitoring Tool字面意思就是JVM统计监控工具。它的核心价值在于实时监控JVM的内存使用和GC活动不用重启应用不用扣CPU太多资源它本身对JVM的开销极小生产环境完全可以直接跑。命令格式长这样jstat -选项 进程ID [采样间隔毫秒] [采样次数]常用选项就这几个选项作用使用频率-class查看类加载/卸载统计中-gc查看堆内存各区域使用情况及GC次数/耗时最高-gcutil查看堆内存使用率百分比比-gc更直观最高-gccause查看GC原因及最近一次GC的统计中-gcnew查看新生代GC详情低-gcold查看老年代GC详情低-compilerJIT编译统计低我用得最多的就是-gcutil和-gc这两个。-gcutil直接给出百分比一眼看出老年代是不是要爆了-gc给出的绝对值字段多适合细看各区域容量。这里有个小坑JDK 9之后进程号默认显示的不是PID而是per-user的进程文件。比如jps列出的可能是某个UID下的进程直接jps -l看全限定主类名就行了。Linux下也可以直接用ps -ef | grep java找到PID然后再喂给jstat。2.2 jstat -gcutil输出字段逐项拆解先看一条实际输出$ jstat -gcutil 12345 1000 5 S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 0.00 12.35 78.42 56.18 92.35 88.10 1280 45.234 8 2.356 47.590逐项解释S0、S1Survivor 0区和Survivor 1区的使用百分比。EEden区使用百分比。OOld区老年代使用百分比。MMetaspace元空间使用百分比JDK 8之后方法区从堆里拆出来的所以单独列。CCSCompressed Class Space压缩类空间使用百分比。YGCYoung GC发生次数。YGCTYoung GC累计耗时单位秒。FGCFull GC次数。FGCTFull GC累计耗时单位秒。GCTGC总耗时就是YGCT FGCT。怎么看这个输出重点看两件事O是不是持续上涨不回落FGC是不是频繁增加。如果老年代使用率从56%一路干到90%以上Full GC次数每几分钟1那基本可以断定老年代压力大得查内存泄漏或者对象分配过多。YGCT / YGC算一下单次Young GC平均耗时如果超过50毫秒说明新生代GC负担已经很重了。GCT是一个累计值可以隔一段时间采样两次做差值算出这段时间GC花了多少时间。比如间隔10分钟两次采样GCT差额是30秒意思是JVM每秒有5%的时间花在GC上这通常是个危险信号。2.3 jstat -gc输出与堆配置对照-gc输出的是绝对值更适合和启动参数里的堆配置对照S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT 1536.0 1536.0 0.0 240.2 12288.0 9682.0 25600.0 14387.2 20480.0 18902.3 2560.0 2256.8 1280 45.234 8 2.356 47.590字段含义S0C、S1C两个Survivor区容量单位KB。S0U、S1U两个Survivor区已使用量。ECEden区容量。EUEden区已使用量。OC老年代容量。OU老年代已使用量。MC元空间容量committed。MU元空间已使用量。CCSC压缩类空间容量。CCSU压缩类空间已使用量。干活的时候拿OU / OC算老年代实际使用率比如14387.2 / 25600.0大约是56%。如果这个比例长期稳定在70%以内说明内存水位正常如果持续走高不回落就要怀疑是不是有对象长期驻留在老年代比如静态集合、缓存没有清除、ThreadLocal使用不当等。还有一个细节S0C和S1C容量不一定一样大。Survivor空间会动态调整这取决于JVM的AdaptiveSizePolicyJDK 8默认开启。如果不想让它变启动参数加-XX:-UseAdaptiveSizePolicy直接关掉。2.4 示例定位一次典型的Full GC频繁我记录过这样一个真实案例。某服务突然报警频繁Full GC拿到PID后先用jstat -gcutil连续采了10次每次1秒jstat -gcutil 25896 1000 10输出的趋势非常清晰Eden区占用持续在80%~95%之间徘徊每次Young GC之后虽然Eden降下来了但老年代占用一路从60%爬升到了94%。到第10次采样时FGC从3跳到7说明这4秒内发生了4次Full GC。这就暴露了一个典型问题对象晋升率太高。大概率是短生命周期的大对象直接进了老年代或者设置的-XX:MaxTenuringThreshold太高导致对象在Survivor区撑不满就提前晋升再或者Survivor空间太小对象在Young GC时放不下就直接跑到老年代。当时我配合jmap -histo看了一眼实例分布定位到某个缓存类实例数量异常最后重启后用-Xmx、-XX:NewRatio调整了分代比例问题才稳定下来。这个组合拳流程下面的章节会完整讲。3. jmap实操堆内存分布与堆快照分析3.1 jmap两大用途看统计和导快照jmap是Java Memory Map的缩写主要解决两类问题第一查看堆内存的概览包括堆配置、各代使用情况、类加载器统计、对象分布直方图。这类操作开销小可以在生产环境直接用。第二导出堆转储快照Heap Dump把整个堆内存的状态写到一个.hprof文件里然后带回来用MAT、VisualVM、JProfiler这些工具慢慢分析。导出快照是一个重量级操作生产环境谨慎使用原因我下面会细说。最常用的几个命令# 查看堆概览包括GC配置参数 jmap -heap PID # 查看类直方图按对象数量或占用内存排序 jmap -histo PID jmap -histo:live PID # 导出堆转储文件 jmap -dump:formatb,file/tmp/heap.hprof PID jmap -dump:live,formatb,file/tmp/heap_live.hprof PID3.2 jmap -heap怎么看实际输出长这样Attaching to process ID 25896, please wait... Please consult the documentation in the HotSpot Diagnostic Commands... Debugger attached successfully. Heap Configuration: MinHeapFreeRatio 40 MaxHeapFreeRatio 70 MaxHeapSize 4294967296 (4096.0MB) NewSize 891289600 (850.0MB) MaxNewSize 891289600 (850.0MB) OldSize 3403677696 (3246.0MB) NewRatio 2 SurvivorRatio 8 Heap Usage: New Generation (Eden 1 Survivor Space): capacity 805306368 (768.0MB) used 623259648 (594.4MB) free 182046720 (173.6MB) 77.4% used Eden Space: capacity 715128832 (682.0MB) used 623243824 (594.4MB) free 91885008 (87.6MB) 87.1% used From Space: capacity 90177536 (86.0MB) used 0 (0.0MB) free 90177536 (86.0MB) 0.0% used To Space: capacity 90177536 (86.0MB) used 0 (0.0MB) free 90177536 (86.0MB) 0.0% used Old Generation: capacity 3403677696 (3246.0MB) used 3152450632 (3006.3MB) free 251227064 (239.7MB) 92.6% used重点看Old Generation的used占比。上面这个例子老年代已经92.6%这基本就是危险水位。因为CMS的触发阈值默认是92%G1也有类似机制老年代到这个程度很快就会触发新一轮Full GC而且是连续多轮的恶性循环。Heap Configuration里的NewRatio 2意味着老年代是新生代的2倍SurvivorRatio 8意味着Eden是单个Survivor的8倍。这些参数直接决定了对象在分代之间怎么流转如果看到Survivor空间特别小、Young GC又频繁可以想想是不是SurvivorRatio设置不合理。3.3 jmap -histo定位问题对象-histo输出的是所有类的实例统计按实例数量或者占用总内存排序。看前20行就够了命令jmap -histo:live 25896 | head -20示例输出片段num #instances #bytes class name (module) ------------------------------------------------------- 1: 1260090 201614400 [B 2: 987456 165682938 java.util.HashMap$Node 3: 234567 98723456 com.example.cache.LocalCacheEntry 4: 200000 64000000 [Ljava.lang.Object;[B是byte数组经常是大头因为很多IO读取、网络报文、图片处理都会产生byte数组。但真正要警惕的是第3行这种业务类实例——LocalCacheEntry有23万个每个大概400多字节加起来接近100MB。如果你的业务逻辑里根本不该有这个数量级的对象那基本就是泄漏点或者错误设计比如把缓存对象放进了一个无上限的Map里。-histo:live会先触发一次Full GC把没引用的垃圾对象清掉然后再统计存活对象。这样看到的是割掉垃圾之后谁还活着更有借鉴意义。但代价是生产环境会卡顿如果服务对延迟极其敏感慎用这个参数先跑不带live的命令差别不大就先顶着。3.4 堆Dump导出与MAT分析入门导出命令很简单jmap -dump:formatb,file/opt/logs/heap_$(date %Y%m%d_%H%M%S).hprof 25896导出的.hprof文件大小基本等同于当前堆的已使用量3GB的堆导出来可能就2GB多。生成文件的过程中JVM会暂停一段时间因为要冻结堆状态所以生产环境导出要挑流量低峰期或者干脆做好监控告警在一轮GC刚结束的时候导尽量减少影响。拿到文件后用MATEclipse Memory Analyzer打开最常用的几个视角Leak SuspectsMAT自动帮你圈出最疑似内存泄漏的地方。Dominator Tree看哪个对象支配的内存最多。Histogram按类统计实例数和占用内存。GC Roots路径选中一个可疑对象查看从GC Root到它的引用链判断是不是被无意识持有。如果没有MAT线上机器也可以先用jhat打开JDK 8自带但非常难用浏览器访问http://localhost:7000查堆里的对象。JDK 9之后jhat被移除了所以现在主流还是本地用MAT分析。需要特别留意导出快照时如果工具版本和JDK版本严重不一致快照可能打不开。jmap -dump生成的hprof用MAT分析时尽量用最新的MAT版本老版本对JDK 17之后新增的某些对象头字段解析不强容易报错。4. jstack实战线程快照与死锁排查4.1 jstack能看到的线程信息jstack输出的是JVM所有线程此刻的栈快照相当于给每个线程拍了一张正在干什么的照片。它解决的是这类问题CPU飙高不知道是哪个线程干的接口卡住不动怀疑是死锁线程池里的线程都阻塞了找不出原因。命令格式jstack -l PID jstack PID /tmp/thread_dump_$(date %Y%m%d_%H%M%S).txt-l参数会额外展示锁的持有情况排查死锁必须加。jstack输出内容很长每个线程一个段落头部是线程的描述信息http-nio-8080-exec-123 #123 daemon prio5 os_prio0 cpu123.45ms elapsed2345.67s tid0x00007f8c8c123 nid0x3e8 waiting on condition [0x00007f8c712ab000] java.lang.Thread.State: TIMED_WAITING (parking) at jdk.internal.misc.Unsafe.park(Native Method) at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:234) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2085) at java.util.concurrent.ArrayBlockingQueue.poll(ArrayBlockingQueue.java:439) at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1073) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) at java.lang.Thread.run(Thread.java:832)每行都有用http-nio-8080-exec-123线程名Tomcat的线程池线程命名方式就是这种。#123线程编号。daemon是否为守护线程。prio5、os_prio0线程优先级。tidJVM内部线程ID。nid0x3e8操作系统线程ID十六进制这个值最有用排查高CPU线程全靠它。java.lang.Thread.State线程当前状态关键判断依据。栈帧列表从当前正在执行的位置一路往上直到线程入口。4.2 高CPU线程定位的经典五步这是个高频场景我直接给完整步骤第一步用top找出CPU占用最高的java进程PIDtop -c按P键按CPU占用排序记下java进程的PID比如25896。第二步用top -Hp 25896找出该进程内CPU占用最高的线程IDtop -Hp 25896这时候看到的是一个十进制的线程号比如18975。第三步把十进制线程号转成十六进制printf %x\n 18975输出结果是4a1f。为什么要转因为jstack里的nid是十六进制的不转找不到对应线程。第四步执行jstack并搜索jstack 25896 /tmp/thread_dump.txt grep -A 20 nid0x4a1f /tmp/thread_dump.txt-A 20意思是从匹配行下面再取20行看到整个线程栈。第五步分析线程栈顶的方法看它在忙什么。如果是java.lang.Thread.State: RUNNABLE并且栈顶是业务代码说明CPU真的是在执行这段逻辑如果栈顶是Object.wait、LockSupport.park这类阻塞调用说明它虽然在CPU列表里靠前但可能只是短暂进入又退出要多采样几次确认。如果CPU高的线程堆栈指向了GC线程比如VM Thread、G1 Young RemSet Sampling Thread那问题根源又回到GC和小面说的堆内存问题上去了需要配合jstat看GC趋势。4.3 如何判断死锁死锁的特征是两个或多个线程循环等待对方持有的锁谁都不让。jstack -l输出的最末尾会专门列出检测到的死锁Found one Java-level deadlock: thread-2: waiting to lock monitor 0x00007f8c8c003c98 (object 0x000000076b34cbd0, a java.lang.String) which is held by thread-1 thread-1: waiting to lock monitor 0x00007f8c8c003cb8 (object 0x000000076b34cbe0, a java.lang.String) which is held by thread-2 Java stack information for the threads listed above: thread-2: at com.example.DeadLockDemo.methodB(DeadLockDemo.java:35) - waiting to lock 0x000000076b34cbd0 (a java.lang.String) at com.example.DeadLockDemo.methodA(DeadLockDemo.java:25) - locked 0x000000076b34cbe0 (a java.lang.String) ...不用看太多Found one Java-level deadlock一行出来就是盖棺定论了。往下看是哪些线程、锁了哪个对象、阻塞在哪一行代码信息全都有。排查死锁有个重要的注意点jstack是瞬间快照死锁是持续状态所以一拍一个准。但如果是间歇性死锁或者锁等待超时导致的不完全死锁可能多拍几次、间隔几秒拍几张才能抓到。我习惯连续拍3~5次间隔3秒写成脚本循环执行拿到多个快照再对比。4.4 线程状态速查什么状态算正常什么算异常java.lang.Thread.State一共有NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED六种状态jstack里最常见的就三种状态含义正常吗RUNNABLE正在执行中或等待CPU调度正常TIMED_WAITING带超时的等待如sleep、wait(1000)、poll(timeout)大部分正常WAITING无时限等待如Object.wait()、LockSupport.park分情况BLOCKED阻塞等待获取锁少量正常太多就有问题如果BLOCKED的线程有成片出现的趋势比如40个线程里30个都是BLOCKED而且各自栈里waiting to lock 同一把锁说明服务出现严重锁竞争。最常见于数据库连接池被打满、线程池队列写满而业务代码还在同步等待。还有一种情况大量线程堆积在TIMED_WAITING但看栈顶都是ThreadPoolExecutor.getTask或者ArrayBlockingQueue.poll那是线程池里空闲线程在等待任务很正常不是问题。5. 综合案例一起CPU飙高频繁GC的真实排查复盘5.1 故障现象与初步判断有一次线上服务告警某节点CPU直接冲到300%多接口超时率暴涨。我先用top确认是java进程的PID31127然后第二步用top -Hp 31127看到两个线程占用特别高一个20312、一个20315转成十六进制分别是4f58和4f5b。紧接着执行jstack 31127 /tmp/jstack_31127_1.txt jstat -gcutil 31127 1000 10 /tmp/jstat_31127.log5.2 jstack找到元凶grep nid0x4f58 -A 30 /tmp/jstack_31127_1.txt看到栈顶是一个自定义类OrderService.process()状态是RUNNABLE。再查nid0x4f5b发现同一个方法。说明CPU高是业务逻辑在疯狂执行不是GC线程之类的虚高。再看其他线程发现一大批http-nio-8080-exec-*都在WAITING状态栈顶是SocketInputStream.read说明Tomcat线程全堵在等请求响应上典型的请求堆积但没有可用线程消化的征兆。5.3 jstat定位GC压力jstat -gcutil 31127 1000 10的输出里老年代使用率从75%一路涨到96%FGC从原来的12次增长到了32次。Young GC次数增长倒是不大但每次Full GC耗时都在500毫秒以上。这就形成了恶性循环业务逻辑里有大量的对象创建Eden区频繁触发Young GC部分对象晋升到老年代老年代很快满了触发Full GCFull GC期间业务线程停顿导致请求处理变慢请求堆积又产生更多对象进一步加剧GC。CPU飙高的直接原因反而变成次要因素——Full GC期间的GC线程和JIT编译把CPU吃掉了。5.4 jmap -histo证实泄漏点jmap -histo:live 31127 | head -30的结果让人很意外排第一的[B有接近50万个实例占用超过800MB排第三的是一个工具类里的静态Map结构static final MapString, Listbyte[]。查代码发现这个Map是某个临时缓存的实现本以为只是短暂保存结果写代码的人只加了读和写忘记清理导致每个上传文件的字节数组都被永久引用了。这就是标准的堆内存泄漏。最终处理临时修复直接重启服务长期修复是改造缓存逻辑用Guava的CacheBuilder或者Caffeine设置最大容量和过期时间同时加了监控指标。重启后再用jstat -gcutil采样老年代使用率稳定在40%左右Full GC频率降为零。5.5 命令组合使用的先后顺序这次复盘下来排查顺序的优先级很重要。我个人建议的套路是top看进程和线程确认是CPU型问题还是IO型问题。jstat -gcutil连续采样几十秒确认GC状态是否健康。如果GC正常说明问题在业务代码逻辑上直接jstack。jstack拍线程快照定位是哪些线程在消耗CPU、有没有死锁。如果GC不正常jmap -histo看对象分布如果怀疑堆过大且不方便直接导hprof先看直方图速判再决定要不要jmap -dump导出。反复对比多张线程快照、GC日志排除偶发因素。这个顺序最大的好处是每一步伤害都不大且信息量递增。一上来就jmap -dump容易造成长时间STW搞不好直接拖垮服务得不偿失。6. 生产环境避坑指南与替代工具6.1 jmap -dump要慎用的三个理由很多人排查内存问题时会习惯性直接jmap -dump但生产环境这么做风险很高我的态度是能不用就不用。第一个理由是STW时间不可控。堆里有几个GB对象时导出快照要冻结整个JVMSTW时间可能长达几十秒线上服务直接就断连了。第二个理由是磁盘空间不可控。堆用了4GB导出的文件可能接近4GB如果没注意磁盘水位直接把监控盘写爆变成二次事故。第三个理由是工具版本和JVM架构要匹配。个别情况下64位JVM导出的hprof在32位MAT里打不开换工具又影响分析进度。如果只是想看堆用量jstat -gc或者jmap -heap就够了。真需要导出完整堆分析泄漏我建议用jcmdjcmd 31127 GC.heap_dump /tmp/heap_$(date %Y%m%d).hprofjcmd是JDK 7之后官方推荐的统一诊断命令JDK 8及以上都可用。它支持很多子命令比如GC.heap_dump、Thread.print、VM.system_properties等用jcmd PID help可以列出全部支持项。它是逐步替代jmap和jstack的新一代方案JDK 8里已经有雏形JDK 11之后功能已经很完善了。6.2 JDK版本差异与工具缺失的适配方案前面提过JDK 12之后Oracle JDK移除了jmap -histo和jmap -dump:formatb这两类操作但很多OpenJDK发行版仍然保留。如果手头的环境里jmap -dump报-dump option not supported可以用这些替代方案导出堆快照jcmd PID GC.heap_dump filename/path/to/file.hprof打印线程栈jcmd PID Thread.print查看类直方图jcmd PID GC.class_histogram查看JVM启动参数jcmd PID VM.flags查看系统属性jcmd PID VM.system_properties另外如果实在找不到jstat、jmap、jstack临时应急还可以用jdb早就有了、jhsdbJDK 9之后引入的底层调试工具但这些工具使用门槛更高不属于三件套的范畴我就不展开讲了。6.3 不要忽略GC日志这个先行的信息来源jstat是实时采样的工具看的是当前状态。但如果服务重启过或者你需要历史趋势jstat就帮不上忙了。这时候应该看GC日志。JDK 8的GC日志启动参数-Xloggc:/opt/logs/gc.log -XX:PrintGCDetails -XX:PrintGCDateStampsJDK 11用的是统一的日志风格-Xlog:gc*:file/opt/logs/gc.log:time,uptime,level,tags排查老年代上涨的问题GC日志能直接看到每次Full GC前后的堆用量对比能计算晋升速率还能看到GC Cause是Allocation Failure还是Metadata GC Threshold还是System.gc()。这些信息远比jstat的一次采样要丰富得多。所以我的排序是先看GC日志再配合jstat实时确认最后再考虑要不要dump堆。6.4 敏感环境小心操作容器环境与权限问题容器化部署越来越多以后这三个工具的使用场景也会遇到额外限制。一是进程号的问题。在容器里Java进程看到的PID通常是1但宿主机上的PID不是这个。最好通过jps -l找到正确的JVM进程ID或者直接看/proc下的进程来确认。二是权限限制。某些容器以非root用户运行jmap、jstack需要attach到目标进程对操作系统权限有要求。系统报错Unable to open socket file或者permission denied时先确认是不是用户权限的问题而不是工具本身坏了。三是同一个容器内多个Java进程。这种情况少见但存在jps -l会列出全部避免抓错进程把没用的那个dump了。四是安全管控。有些生产环境禁止直接执行jmap -dump需要审批。如果遇到明确的安全策略建议至少提前报备或者用jcmd GC.heap_dump配合工单。7. 实用建议速查我踩过的坑和给你的一些习惯这三件套看着简单但真正用的时候有些习惯培养起来能省不少事。习惯一命令输出别只打屏一定要落到文件。jstack 31127 /tmp/stack_$(date %Y%m%d_%H%M%S).txt jstat -gcutil 31127 1000 10 /tmp/gcutil_$(date %Y%m%d_%H%M%S).log不只是为了留证据更是为了和后面的采样做对比。只有连续多张快照才能排除偶发因素。习惯二jstack多拍几次间隔3~5秒。第一张快照可能是假象比如某个线程只是在某个瞬间碰巧执行到某处。间隔拍几张如果每次都卡在同一个位置那才说明问题是持续的。习惯三jstat的采样时长要盯住。从jstat -gcutil PID 1000 60开始至少采一分钟。一分钟内可以看到几十次采样GC频率和趋势都能看出来。只采一两次没有参考价值。习惯四配合top -Hp看线程CPU别跳过。jstack本身不告诉你哪个线程CPU高必须先靠top -Hp找到线程再转十六进制去jstack里搜。这是最常用的组合拳也是最快定位高CPU问题的方法。习惯五修复后一定要复测。很多人改完代码重启服务就完事了没有回去再跑一遍jstat确认GC指标回落。我建议这步必须做CTO问起来你也拿得出数据。另外关于JDK环境配置的热词网上很多人在问jdk环境变量配置失败“jdk降级到17”“win11 jdk安装配置”这类问题。这里多说一句排查JVM问题时确保JAVA_HOME和PATH里的java、jstat、jmap、jstack都指向同一个JDK版本。如果你机器上装了多个JDKwhich java看了是JDK 17jps却来自JDK 8那jstat可能会因为attach机制版本冲突而连不上目标进程。这个坑我踩过不只一次建议用java -version和jps -V同时验证。这三件套完全是冷兵器时代的产物但正因为JDK自带的反而在任何环境都最可靠。它们解决不了所有问题但能帮你快速判断问题在GC、内存还是线程层面再决定要不要上MAT、Arthas这种重型工具。排查思路比工具本身更重要先看现象收敛方向再选工具顺序对了问题的答案往往就自己浮出水面了。
返回列表