Java内存泄漏检测工具实战:线上卡顿别只会重启,用这些JVM工具定位根因

检测维修 0 76

大家好,我是一安~

帮忙给朋友排查一个怪异的问题, 线上服务每隔两三天就会出现卡顿现象, 并且重启之后就恢复正常状态, 然而过两天就又发生卡顿。他满脸无奈地讲: “我都是依靠重启来解决, 反正重启就行。”。

然而, 重启并不意味着问题能够得到解决, 下一回卡顿依旧会出现, 并且会愈发频繁。随后, 我借助几个JVM诊断工具, 确定是线程池已满致使任务堆积, 根本原因是某一个下游接口超时, 从200毫秒增长到了5秒, 连接池被全部占用后, 所有请求都在排队等候。

这个文章, 将JVM诊断工具的实战运用方法, 进行了一番条理清晰的梳理, 下次要是线上再度出现问题, 可不要再仅仅只会采取重启这种单一解决方式了。

一、JVM诊断工具全景图

别急着马上动手, 弄明白每一个工具能够做些什么, 会比没头没脑盲目去敲命令要更加重要出许多、许多。

工具

核心能力

适用场景

是否影响性能

监控GC和内存统计

观察GC频率、各代内存变化

堆内存快照、对象统计

OOM排查、内存泄漏定位

有STW风险

线程堆栈快照

CPU飙高、线程死锁、线程阻塞

查看和修改运行时参数

确认JVM参数、动态开关GC日志

全方位在线诊断

热更新、方法追踪、实时监控

有一个关键的原则, 那就是要是能够先用轻量工具去进行排查的话, 那就不要一开始就直接使用jmap dump。因为若dump整个堆的话, 这不仅会触发STW, 而且生成的文件会非常大, 比如对于16G的堆而言, dump出来的文件就是16G, 大到传输都成问题。

二、jstat:GC问题的第一道防线

jstat 是 JVM 监控工具中开销最小的,其所称不会暂停应用。它适合于线上持续观察。

2.1 常用命令

查看GC概况 :

# 每隔1秒输出一次,共输出10次
jstat -gc 1000 10

输出结果:

 S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
10240 10240 0.0 6144.0 81920.0 20480.0 204800.0 102400.0 51200.0 49024.0 6144.0 5888.0 1234 12.345 56 8.765 21.110

这些缩写看着头疼?记住这个规律:

查看GC百分比 :

# 用百分比显示,更直观
jstat -gcutil 1000 5

输出:

 S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 60.00 25.00 50.00 95.75 96.00 1234 12.345 56 8.765 21.110

改成百分比形式会更易于察觉到问题, 一旦瞧见O区长期处于90%以上, 并且FGC频繁出现, 基本上就意味着存在内存泄漏或者堆空间不足的状况了。

2.2 实战排查思路

用jstat排查GC问题的三板斧:

第一步:看FGC频率

# 间隔5秒执行两次,对比FGC次数
jstat -gcutil 5000 2

倘若两次之间, FGC出现了增加的情况, 那就表明, 老年代频繁地触发了Full GC, 而这一般来讲, 属于危险的信号。

第二步:看老年代增长趋势

# 持续观察老年代使用率
jstat -gcutil 1000 30

老年代的使用率, 始终处于上涨状态, 根本降不下来, 哪怕是进行了Full GC操作, 依旧无法降低, 所以基本上能够判断为内存泄漏了。

第三步:看GC耗时占比

GC耗时占比 = GCT / 应用运行总时间

若GC所耗时间在整体时间里的占比这个数值多于5%, 那么应用的性能显著会跌落;占比超过10%, 基本上用户能够察觉到卡顿现象。

三、jmap:OOM排查的核武器

jmap能够把堆内存的详细快照获取到, 它属于OOM排查的终极武器, 可是恰恰由于其威力巨大, 因而在使用之际必须要格外谨慎小心。

3.1 查看堆内存对象统计

先看对象统计,不用dump整个堆,开销小很多:

# 按对象数量排序,查看前20
jmap -histo | head -20

输出:

 num #instances#bytes class name
1: 1234567 123456789 [B (byte数组)
2: 567890 56789012 java.lang.String
3: 345678 22345678 java.util.HashMap$Node
4: 234567 18765432 com.example.order.entity.OrderItem

这儿存在个要点需着重看: 倘若class name身为你自身的业务类而排列于前面, 极有可能是症结之处, 像。

由B(byte数组)、String、HashMap$Node这些JDK内部类排在前面是正常的, 然而要是OrderItem这种业务类实例数可达上百万的情况, 那就需要额外留意了, 要保持警惕了。

3.2 只看存活对象

# 只统计存活对象(会触发Full GC)
jmap -histo:live | head -20

补充说明, live会先引发一次Full GC, 之后再进行统计, 如此一来便能排除即将被回收的对象, 所看到的才是切实驻留在内存里的。将-histo与-histo:live的结果差异作对比, 要是差异极大, 表明存在大量临时对象;要是差异极小, 表明这些对象确实被引用着, 出现了泄漏。

3.3 Dump堆内存

# 生成堆dump文件
jmap -dump:format=b,file=heap.hprof

# 只dump存活对象(先Full GC再dump,文件更小)
jmap -dump:live,format=b,file=heap.hprof

重要警告 :

执行jmap -dump操作, 会引发STW, 致使整个应用处于暂停状态, 与此同时, 堆的规模越大, 那么暂停的时长就会越久。

16G堆dump出来的文件大约16G,确保磁盘空间足够

优先用 :live 参数,dump出来的文件小很多

如果应用已经OOM快要挂了,赶紧dump,这是最后的机会

3.4 分析dump文件

获取dump文件之后, 借助MAT(Memory Analyzer Tool)或者VisualVM进行剖析:

# 如果是线上服务器,dump文件太大传不出来,可以先压缩
gzip heap.hprof
# 压缩后大约是原来的1/3到1/5

MAT分析的核心看三个报告:

易泄漏嫌疑报告: 自动开展检测, 针对有可能出现的内存泄漏点位。

Dominator Tree :看哪些对象占内存最多

Histogram :按类统计对象数量和大小

四、jstack:线程问题的排查利器

CPU出现飙高情况, 线程发生死锁现象, 请求出现超时状况, 这是线上最为常见的三个问题, 而jstack都能够进行排查。

4.1 基本用法

# 打印线程堆栈
jstack

# 强制打印(普通jstack无响应时使用)
jstack -F

4.2 CPU飙高排查(最经典的排查流程)

这是线上排查最频繁的场景,完整流程如下:

第一步:找到Java进程PID

ps -ef | grep java
# 或
jps -l

第二步:找到占用CPU最高的线程

# 找到Java进程中CPU占用最高的线程
top -Hp

假设找到占用CPU最高的线程TID是 12345 。

第三步:线程ID转十六进制

printf"%x\n" 12345
# 输出:3039

第四步:在jstack输出中搜索

jstack  | grep 3039 -A 30

找到对应线程的堆栈,就能定位到是哪行代码在疯狂消耗CPU。

常见的CPU飙高原因:

4.3 死锁检测

# jstack自动检测死锁
jstack

jstack输出末尾会自动打印死锁信息:

Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007f8e1c006898 (object 0x000000076d3a8c80, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00007f8e1c006858 (object 0x000000076d3a8c70, a java.lang.Object),
which is held by "Thread-1"

在看到这般情形的时候, 死锁的状况清晰呈现出来, 具体是, Thread - 0持有锁A, 它正在等待锁B, 而Thread - 1持有锁B, 它正在等待锁A, 这属于典型的那种循环等待情况。

4.4 线程状态速查

jstack输出的线程状态要能看懂:

状态

含义

需要关注吗

正在运行或等待CPU

锁竞争严重时关注

JVM诊断工具实战应用_java内存泄漏检测工具_jstat监控GC问题排查

无限期等待(wait/join/park)

线程池满时关注

有超时的等待(sleep/wait(timeout))

大量出现时关注

WAITING on condition

等待某个条件(如IO、锁)

连接池满时关注

五、jinfo:运行时参数查看与修改

Jinfo, 是一个, 容易被, 忽视的, 工具, 然而, 在排查, JVM参数, 相关问题, 的时候, 却很好用。

5.1 查看所有JVM参数

# 查看当前JVM的所有参数
jinfo -flags

输出:

VM Flags:
-XX:CICompilerCount=4 -XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296
-XX:+UseG1GC -XX:+PrintGCDetails -XX:+PrintGCDateStamps ...

什么时刻使用呢? 当你对线上JVM参数未生效产生怀疑的时候。曾经有一回排查GC相关问题, 查看日志时始终感觉并非G1的表现, 运用jinfo去查看, 哎呀不得了, 居然用的是CMS, 启动脚本里的那个 -XX:+UseG1GC被另外一个脚本给覆盖掉了。

5.2 查看具体参数

# 查看某个具体参数的值
jinfo -flag UseG1GC
# 输出:-XX:+UseG1GC

jinfo -flag PrintGCDetails
# 输出:-XX:+PrintGCDetails

5.3 动态修改参数(慎用)

# 动态开启GC日志(不需要重启应用!)
jinfo -flag +PrintGCDetails
jinfo -flag +PrintGCDateStamps

# 动态关闭
jinfo -flag -PrintGCDetails

关键要留意, 并非所有参数均支持动态调整, 唯有被标注为可管理的参数方可进行动态修改, 去查看究竟哪些参数是能够从事动态修改工作的:

java -XX:+PrintFlagsFinal -version | grep manageable

六、Arthas:JVM诊断的天花板

那排在前面的4个, 是JDK自身所带的工具。而Arthas, 它是由阿里进行开源的。其能力把上面提到的所有工具都涵盖了, 并且还额外拥有一大堆具备黑科技性质的功能。

6.1 安装和启动

# 下载
curl -O https://arthas.aliyun.com/arthas-boot.jar

# 启动(会列出所有Java进程,选择要诊断的)
java -jar arthas-boot.jar

6.2 核心命令实战

dashboard:全局监控面板

dashboard

一屏可将线程, 内存, GC等关键信息予以展示, 这等同于同时去看jstat与jstack, 并且还会实时进行刷新。

thread:线程排查

# 查看CPU占用最高的3个线程
thread -n 3

# 查看所有BLOCKED状态的线程
thread -b

# 查看指定线程的堆栈
thread

执行thread -n 3, 一下子便能揪出致使CPU飙高的罪魁祸首,相较于jstack加上top的那一趟流程, 速度要快出许多。

watch:方法执行观测

# 观测方法入参、返回值和异常
watch com.example.order.service.OrderService createOrder "{params,returnObj,throwExp}" -x 3

这个功能强大得很厉害——针对于此, 是并不必须要改动代码再来添加日志这个行为 , 做到在线上环境直接去观测方法执行的具体状况 , 而 -x 3 所呈现的意思是展露出三层属性。

trace:方法调用链耗时

# 追踪方法调用链路和每个子调用的耗时
trace com.example.order.service.OrderService createOrder

输出效果:

+---[200ms] com.example.order.service.OrderService.createOrder
+---[5ms] com.example.order.dao.OrderDao.insert
+---[180ms] com.example.order.client.PaymentClient.pay
+---[15ms] com.example.order.service.InventoryService.deduct

第一眼看过去, 就能瞧见, PaymentClient.pay 所花费的时间是 180ms, 它占据了总体所耗费时间的 90%, 至此, 关于瓶颈的定位工作已然完成。

sc:查看已加载的类

# 搜索已加载的类
sc com.example.order.*

# 查看类的详细信息
sc -d com.example.order.service.OrderService

heapdump:堆内存快照

# dump堆内存(和jmap -dump一样)
heapdump /tmp/heap.hprof

# 只dump存活对象
heapdump --live /tmp/heap.hprof

6.3 Arthas vs JDK工具对比

场景

JDK工具

Arthas

优势

thread -n 3

方法耗时分析

只能看日志或加代码

trace

方法参数观测

watch

heapdump

vmoption

阿尔萨斯具备的最为突出的优势在于, 无需重新启动应用程序, 无需对代码进行修改, 能够实现在线诊断。

七、三大经典排查场景 场景1:CPU飙高排查

1. top -Hp  找到高CPU线程
2. printf"%x\n" 转十六进制
3. jstack | grep -A 30 看堆栈
4. 分析堆栈定位代码行

或直接用Arthas:thread -n 3,一步到位

出现踩坑情况需要提醒, 要是因为GC致使CPU方面的数值变高, 通过jstack所看到的线程是在干活的GC线程, 并非是业务线程出现的问题。首先要看top里面的CPU是us(也就是用户态)较高还是sy(也就是内核态)较高, 要是sy较高并且GC线程处于活跃状态, 很大概率是GC方面存在的问题。

场景2:OOM排查

1. jstat -gcutil  观察各代内存和GC情况
2. jmap -histo:live 查看存活对象分布
3. jmap -dump:live,format=b,file=heap.hprof dump堆
4. 用MAT分析dump文件
5. 定位泄漏对象和引用链

踩坑提醒:如果应用已经因为OOM要挂了,加上 -XX:+

有一个名为HeapDumpOnOutOfMemoryError的启动参数, 当出现OOM的时候, 它能够自动进行dump。千万不要等到出了事之后, 才想起来这件事。

场景3:线程死锁排查

1. jstack  自动检测死锁
2. 或者Arthas:thread -b 查看阻塞线程
3. 根据堆栈分析锁的获取顺序
4. 调整代码中的加锁顺序

进行踩坑提示, 死锁并非必然仅存在于两个线程之中, 有可能出现多个线程形成循环等待的这般状况。jstack会将完整的等待链予以打印, 千万别仅仅查看第一组就觉得大功告成了。

八、踩坑经验

坑1:jmap导致应用长时间STW

有一回, 在流量处于尖峰时期去执行jmap -dump命令, 堆内存为16G的应用发生了STW状态, 持续时间差不多有30秒, 导致整个网关出现了超时告警的情况, 从这以后我由此学到了:

坑2:jstack获取不到线程信息

有时, jstack报错, 显示为Unable to open socket file, 而其原因一般是:

处置方式为: 采用jstack -F进行强制打印, 又或者借助Arthas, 而Arthas具备更为强大的attach能力。

坑3:dump文件太大无法分析

32G的堆被dump出来形成32G的文件, MAT默认情况下仅仅能容纳4G内存, 结果直接导致OOM。

解决办法:

# 1. 压缩后传输
gzip heap.hprof

# 2. MAT配置大内存,修改MemoryAnalyzer.ini
-Xmx8g

# 3. 只dump存活对象
jmap -dump:live,format=b,file=heap.hprof

坑4:忘了加启动参数

线上OOM了,结果没加 -XX:+

HeapDumpOnOutOfMemoryError, 不存在任何现场的信息, 仅仅只能干瞪着眼睛, 没有其他办法。

建议所有Java应用启动时加上这些参数:

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/logs/heapdump.hprof
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/data/logs/gc.log

九、面试加分Q&A

Q1:线上CPU飙高,你怎么排查?

首先, 借助top来确认究竟是用户态CPU处于高位还是系统态处于高位。要是用户态处于高位, 那就运用top -Hp去寻觅高CPU线程, 将其转换为十六进制之后, 通过jstack来进行代码定位;要是系统态处于高位, 大概是由于GC频繁所致, 此时运用jstat去观察GC频率。要是出现持续的Full GC, 那就进一步借助jmap来排查内存泄漏。

对于问题二: “运用jmap -dump会引发什么样的问题? 在何种场景之下是不可以使用的? ”。

jmap -dump这个操作会引发STW, 堆的规模越大那么暂停的时间就会越长久。在流量处于高峰期的时候禁止随意进行dump, 对于16G的堆有可能暂停超过30秒, 进而致使大量的请求出现超时的情况。建议在低峰期进行dump, 或者采用 -XX:+。

自动dump要提前配置好, HeapDumpOnOutOfMemoryError。

问三: 阿萨兹和Java开发工具包自带工具的差异?在怎样的情景之下更倾向挑选阿萨兹?

JDK 工具进行的是“快照式”诊断, 所获取的是某一时刻的状态, 而 Arthas 属于“动态式”诊断, 能够实时对方法执行开展观测。当需要在无引发侵入的情形下查看方法入参、返回值以及调用耗时, 又或者是在线上进行热更新代码之际, 应当优先选用 Arthas。不过, Arthas 自身对于应用而言存在一定的性能开销, 在核心链路方面应当谨慎使用 watch 和 trace。

Q4:如何判断是内存泄漏还是内存不够?

存在两个判断标准, 其一, Full GC过后, 老年代使用率依旧无法下降, 这表明存在泄漏情况, 即对象虽被引用但无法回收;其二, Full GC之后, 老年代使用率显著下降, 然而很快又回升, 这意味着内存确实不足, 对象均处于存活状态, 只是空间不够。就前者而言, 需要对泄漏引用链进行定位。就后者而言, 需要增加内存或者优化对象生命周期。

问5: jstat呈现出FGC始终处于上涨的状况, 然而应用看上去却是正常的态势, 这样的情况需不需要进行处理呢?

参照两个指标, 其一, FGC频率, 要是几秒便出现一次Full GC , 且不论应用看似是否正常, 在不久将来必然会出现问题;其二, Full GC耗时, 一旦单次Full GC的时间超出1秒, 用户便能够察觉到卡顿情况。建议设定 GC告警事宜: 倘若Full GC间隔小于60秒, 或者单次所耗时间超出2秒, 就要发出告警。

十、总结

回到开头的问题:JVM出问题只会重启?

5个工具,3套排查流程,记住这个决策树就够了:

最后还要着重指出一点, 线上进行排查的时候, 要先开展轻量级的操作, 之后才进行重量级的操作, 先使用jstat去查看大概的状况, 接着运用jstack/jmap -histo来进行定位, 最后才实施dump操作, 千万不要一开始就进行dump, 因为STW所产生的影响有可能比问题本身造成的影响还要大。

如果觉得有帮助,欢迎转发给需要的朋友

相关推荐: