news 2026/9/30 3:10:23

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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、S1:Survivor 0区和Survivor 1区的使用百分比。
  • E:Eden区使用百分比。
  • O:Old区(老年代)使用百分比。
  • M:Metaspace(元空间)使用百分比,JDK 8之后方法区从堆里拆出来的,所以单独列。
  • CCS:Compressed Class Space,压缩类空间使用百分比。
  • YGC:Young GC发生次数。
  • YGCT:Young GC累计耗时,单位秒。
  • FGC:Full GC次数。
  • FGCT:Full GC累计耗时,单位秒。
  • GCT:GC总耗时,就是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区已使用量。
  • EC:Eden区容量。
  • EU:Eden区已使用量。
  • OC:老年代容量。
  • OU:老年代已使用量。
  • MC:元空间容量(committed)。
  • MU:元空间已使用量。
  • CCSC:压缩类空间容量。
  • CCSU:压缩类空间已使用量。

干活的时候,拿OU / OC算老年代实际使用率,比如14387.2 / 25600.0大约是56%。如果这个比例长期稳定在70%以内,说明内存水位正常;如果持续走高不回落,就要怀疑是不是有对象长期驻留在老年代,比如静态集合、缓存没有清除、ThreadLocal使用不当等。

还有一个细节:S0C和S1C容量不一定一样大。Survivor空间会动态调整,这取决于JVM的AdaptiveSizePolicy,JDK 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:format=b,file=/tmp/heap.hprof <PID> jmap -dump:live,format=b,file=/tmp/heap_live.hprof <PID>

3.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:format=b,file=/opt/logs/heap_$(date +%Y%m%d_%H%M%S).hprof 25896

导出的.hprof文件大小基本等同于当前堆的已使用量,3GB的堆导出来可能就2GB多。生成文件的过程中JVM会暂停一段时间,因为要冻结堆状态,所以生产环境导出要挑流量低峰期,或者干脆做好监控告警,在一轮GC刚结束的时候导,尽量减少影响。

拿到文件后用MAT(Eclipse Memory Analyzer)打开,最常用的几个视角:

  • Leak Suspects:MAT自动帮你圈出最疑似内存泄漏的地方。
  • 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 prio=5 os_prio=0 cpu=123.45ms elapsed=2345.67s tid=0x00007f8c8c123 nid=0x3e8 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:是否为守护线程。
  • prio=5、os_prio=0:线程优先级。
  • tid:JVM内部线程ID。
  • nid=0x3e8:操作系统线程ID,十六进制,这个值最有用,排查高CPU线程全靠它。
  • java.lang.Thread.State:线程当前状态,关键判断依据。
  • 栈帧列表:从当前正在执行的位置一路往上,直到线程入口。

4.2 高CPU线程定位的经典五步

这是个高频场景,我直接给完整步骤:

第一步:用top找出CPU占用最高的java进程PID:

top -c

按P键按CPU占用排序,记下java进程的PID,比如25896。

第二步:用top -Hp 25896找出该进程内CPU占用最高的线程ID:

top -Hp 25896

这时候看到的是一个十进制的线程号,比如18975。

第三步:把十进制线程号转成十六进制:

printf '%x\n' 18975

输出结果是4a1f。为什么要转?因为jstack里的nid是十六进制的,不转找不到对应线程。

第四步:执行jstack并搜索:

jstack 25896 > /tmp/thread_dump.txt grep -A 20 'nid=0x4a1f' /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.log

5.2 jstack找到元凶

grep 'nid=0x4f58' -A 30 /tmp/jstack_31127_1.txt,看到栈顶是一个自定义类OrderService.process(),状态是RUNNABLE。再查nid=0x4f5b,发现同一个方法。说明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 GC,Full GC期间业务线程停顿,导致请求处理变慢,请求堆积又产生更多对象,进一步加剧GC。CPU飙高的直接原因反而变成次要因素——Full GC期间的GC线程和JIT编译把CPU吃掉了。

5.4 jmap -histo证实泄漏点

jmap -histo:live 31127 | head -30的结果让人很意外:排第一的[B有接近50万个实例,占用超过800MB;排第三的是一个工具类里的静态Map结构,static final Map<String, List<byte[]>>。

查代码发现,这个Map是某个"临时缓存"的实现,本以为只是短暂保存,结果写代码的人只加了读和写,忘记清理,导致每个上传文件的字节数组都被永久引用了。这就是标准的堆内存泄漏。

最终处理:临时修复直接重启服务;长期修复是改造缓存逻辑,用Guava的CacheBuilder或者Caffeine设置最大容量和过期时间,同时加了监控指标。重启后再用jstat -gcutil采样,老年代使用率稳定在40%左右,Full GC频率降为零。

5.5 命令组合使用的先后顺序

这次复盘下来,排查顺序的优先级很重要。我个人建议的套路是:

  1. top看进程和线程,确认是CPU型问题还是IO型问题。
  2. jstat -gcutil连续采样几十秒,确认GC状态是否健康。如果GC正常,说明问题在业务代码逻辑上,直接jstack。
  3. jstack拍线程快照,定位是哪些线程在消耗CPU、有没有死锁。
  4. 如果GC不正常,jmap -histo看对象分布;如果怀疑堆过大且不方便直接导hprof,先看直方图速判,再决定要不要jmap -dump导出。
  5. 反复对比多张线程快照、GC日志,排除偶发因素。

这个顺序最大的好处是每一步伤害都不大,且信息量递增。一上来就jmap -dump容易造成长时间STW,搞不好直接拖垮服务,得不偿失。

6. 生产环境避坑指南与替代工具

6.1 jmap -dump要慎用的三个理由

很多人排查内存问题时会习惯性直接jmap -dump,但生产环境这么做风险很高,我的态度是能不用就不用。

第一个理由是STW时间不可控。堆里有几个GB对象时,导出快照要冻结整个JVM,STW时间可能长达几十秒,线上服务直接就断连了。

第二个理由是磁盘空间不可控。堆用了4GB,导出的文件可能接近4GB,如果没注意磁盘水位,直接把监控盘写爆,变成二次事故。

第三个理由是工具版本和JVM架构要匹配。个别情况下,64位JVM导出的hprof在32位MAT里打不开,换工具又影响分析进度。

如果只是想看堆用量,jstat -gc或者jmap -heap就够了。真需要导出完整堆分析泄漏,我建议用jcmd:

jcmd 31127 GC.heap_dump /tmp/heap_$(date +%Y%m%d).hprof

jcmd是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:format=b这两类操作,但很多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(早就有了)、jhsdb(JDK 9之后引入的底层调试工具),但这些工具使用门槛更高,不属于"三件套"的范畴,我就不展开讲了。

6.3 不要忽略GC日志这个先行的信息来源

jstat是实时采样的工具,看的是"当前"状态。但如果服务重启过,或者你需要历史趋势,jstat就帮不上忙了。这时候应该看GC日志。JDK 8的GC日志启动参数:

-Xloggc:/opt/logs/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps

JDK 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版本。如果你机器上装了多个JDK,which java看了是JDK 17,jps却来自JDK 8,那jstat可能会因为attach机制版本冲突而连不上目标进程。这个坑我踩过不只一次,建议用java -version和jps -V同时验证。

这三件套完全是冷兵器时代的产物,但正因为JDK自带的,反而在任何环境都最可靠。它们解决不了所有问题,但能帮你快速判断问题在GC、内存还是线程层面,再决定要不要上MAT、Arthas这种重型工具。排查思路比工具本身更重要,先看现象收敛方向,再选工具,顺序对了,问题的答案往往就自己浮出水面了。

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

Windows本地HTTPS环境搭建:OpenSSL生成SSL证书与Nginx配置指南

最近重新装了一次开发机&#xff0c;把 Windows 下的本地 HTTPS 环境又完整搭了一遍。起因是一个项目里要调试摄像头和麦克风权限&#xff0c;还有 Service Worker 离线缓存&#xff0c;这些功能在 Chrome 里全都要求 secure context&#xff0c;也就是必须走 HTTPS 或者 local…

作者头像 李华
网站建设 2026/9/30 3:09:53

微服务定时任务的分布式唯一执行:ARQ轻量级调度组件实践

做微服务做了一段时间之后&#xff0c;基本都会碰到一个绕不开的难题&#xff1a;定时任务。Spring Boot自带的Scheduled入手很快&#xff0c;但一旦把它放到多个节点上跑&#xff0c;问题就接踵而来——每个副本都执行一遍&#xff0c;数据重复处理、下游接口被反复调用&#…

作者头像 李华
网站建设 2026/9/30 3:09:09

工业上位机开发实战:C#、WinForm与Modbus通信全解析

刚入行的时候&#xff0c;我在车间里蹲了整整半个月&#xff0c;就为了搞清楚一套老旧的WinForm上位机为什么总是隔三差五丢失数据。那时候我满脑子都是SQL语法和.NET框架&#xff0c;完全没意识到真正难的不是写代码&#xff0c;而是怎么和PLC、传感器、现场操作工打交道。直到…

作者头像 李华
网站建设 2026/9/30 3:09:02

Windows域信任关系失败的5类根因与实战修复

简介&#xff1a;本资源是一份针对Windows域环境中‘工作站和主域间信任关系失败’这一高频故障的实操型解决方案文档&#xff0c;主要面向企业IT运维人员、系统管理员及虚拟化平台&#xff08;如VMware、Hyper-V&#xff09;环境下的技术实践者。文档深入剖析故障成因——域用…

作者头像 李华
网站建设 2026/9/30 3:08:25

修复d3dx10_39.dll丢失的正确思路:本质是DirectX运行库不完整

1. d3dx10_39.dll丢失&#xff1f;先分清问题性质再动手如果你是因为打开某个游戏或者软件突然弹窗提示“找不到d3dx10_39.dll”&#xff0c;先别急着下载补丁、重装系统、甚至重装整个Windows。这类报错在Win10、Win11上实在太常见了&#xff0c;几乎每天都有玩家和同事来问我…

作者头像 李华
网站建设 2026/9/30 3:08:17

Flink与Hive函数生态整合:HiveModule复用与原生聚合加速实战

这几年做数据平台&#xff0c;我见得太多了&#xff1a;Hive 里沉淀了上百个自研 UDF/UDTF/UDAF&#xff0c;从解析日志的正则函数到各种业务口径的聚合&#xff0c;每一个都是踩过坑、对过账的资产。结果一上 Flink 实时计算&#xff0c;很多团队第一反应就是“找人用 Java 重…

作者头像 李华