Arthas实战:Java线上诊断与heapdump分析
凌晨两点报警群里甩出一张内存监控曲线服务OOM在即。我坐在电脑前手头只有一个SSH终端不能重启重启等于销毁现场不能等再等接口就要全军覆没。这个场景我经历过不止一次而每次把我从泥潭里拉出来的都是Arthas准确的说是Arthas配合一套完整的线上诊断思路。这篇内容我打算围绕Arthas的日常诊断、dump对象导出以及导出之后怎么分析这条完整链路展开适合所有用Java做后端、需要亲自处理线上故障的开发和运维同学。你会看到这些命令在真实二进制问题里是如何串起来用的而不是单纯罗列命令清单。1. 从jps到Arthas线上JVM排查工具链里为什么最后都得落到Arthas1.1 JDK自带工具各自管一段但各有明显的盲区先把JDK自带的那几个工具盘一遍。不是凑字数而是只有理解它们的边界你才知道Arthas到底补上了哪块空缺。jps解决的是最基础的问题这台机器上有哪些Java进程、启动参数是什么。它对进程内部的状况一无所知只能告诉你谁活着。jstat是GC场景的老手。jstat -gcutil pid 1000每秒打一次能很快画出Young GC频率、老年代使用率和Full GC次数的趋势。它能告诉你GC出问题了但回答不了哪些对象把老年代撑爆了更回答不了业务线程卡在哪个调用上。jmap是堆分析的主力jmap -histo:live pid可以按实例数和占用空间列出类直方图jmap -dump:live,formatb,fileheap.hprof pid能导出堆快照。但这里有个很多人踩过的坑jmap的live参数会触发一次Full GC。线上正扛着流量的时候执行这条命令轻则接口抖动重则这个副本直接被拖死。而且大堆导出时进程会长时间停顿几十G的堆看起来就像假死非常吓人。jstack看线程栈是一把好手死锁、线程卡在哪个锁上、线程池是否饥饿一把梭就出来了。可它只能抓到执行的那一瞬间线上问题往往有偶发性错过那一秒下一次出现不知道要等多久。jconsole和jvisualvm这类图形化工具开发环境和压测环境很香生产环境基本用不上要开通JMX远程端口多一个安全暴露面而且CPU飙高、接口偶发超时这类问题图形界面并不比命令行更快。工具擅长场景主要局限jps查看进程与启动参数看不到进程内部jstatGC频率、内存趋势回答不了谁占的jmap堆直方图、堆快照live参数触发Full GC大堆导出STW长jstack线程栈快照、死锁静态快照抓不住偶发问题jconsole/jvisualvm本地图形化监控需要JMX端口不适合生产Arthas动态诊断、现场还原需要学习成本增强有少量开销1.2 Arthas介入的是运行过程而不是事后快照上面这些工具共同的尴尬是基本都是一次性快照。问题发生那一下没抓到就再也抓不到了。Arthas的定位完全不同——它attach到目标JVM注入诊断代理在不重启进程的前提下提供持续、交互式的诊断能力。接口偶发超时你可以用trace把调用链路每一层的耗时打出来等下一次超时出现时耗时的瓶颈节点当场暴露不用重新部署就能用watch观察某个方法每次调用的入参、返回值和异常怀疑线上跑的不是最新代码直接jad反编译验证。这套能力来自Java的Instrumentation机制和字节码增强技术。这也是它和jmap、jstack这类静态工具的本质区别Arthas是动态介入进程的运行过程后者只是读取进程的静态视角。理解了这一点你就明白为什么所有线上JVM排查终极指南最后都会推荐Arthas。2. 把Arthas挂到线上进程attach机制、启动方式与那些让人抓狂的权限坑2.1 启动从下载到attach的三步操作Arthas启动本身不复杂复杂的是环境。先看标准姿势curl -O https://arthas.aliyun.com/arthas-boot.jar java -jar arthas-boot.jar启动后会列出本机所有Java进程输入序号回车即可attach。目标PID明确时直接指定更快java -jar arthas-boot.jar 12345进入交互界面后输入dashboard回车就能看到进程的实时状态。如果不想进交互模式Arthas也支持一次性执行命令退出java -jar arthas-boot.jar 12345 -c dashboard -n 1这在写定时采集脚本时非常有用。还可以用批处理文件把多组诊断命令串起来执行。2.2 attach的原理和三个高频启动坑arthas-boot向目标进程发起attach时底层调用的其实是JVM的com.sun.tools.attachAPI相当于往目标JVM里加载一个Agent由它执行后续所有诊断命令。这个机制很优雅但实际开工时总有几个坑反复出现。第一个坑用户权限不一致。attach要求执行用户与目标进程的启动用户一致否则会被拒绝。常见场景是进程是tomcat用户启动的你sudo到root去执行Arthas然后报错说无法打开socket文件。原因是/tmp下的attach socket文件属主不一致。解决办法是切到同一个用户再执行或者检查/tmp下hsperfdata_*和.attach_pid*文件的属主。第二个坑临时目录权限。Arthas通过/tmp与目标进程通信如果/tmp被安全策略设置成只读或noexecattach会静默失败。容器环境特别容易出现很多基础镜像对/tmp做了限制。第三个坑JDK版本兼容。JDK 9之后的模块化系统限制了attach API个别场景需要在启动参数加-Djdk.attach.allowAttachSelftrue。另外老版本Arthas挂JDK 11以上进程会出问题我的建议是直接用最新版别在生产上验证老版本的兼容性。2.3 生产环境使用Arthas之前先想清楚三件事先说增强开销。Arthas的字节码增强本身很轻但诊断命令一直挂着对性能是有影响的。我的习惯是定位完立刻stop卸载Agent不要长期挂机。其次Arthas默认会在目标进程开一个Telnet端口3658和WebSocket端口8563生产环境要控制暴露面绑定内网IP并设置认证。再有就是操作留痕。谁在什么时间对哪个进程执行过什么命令要可追溯。工具越强大越需要边界。3. dashboard、thread、watch、trace线上问题排查最高频的四个命令与真实案例工具装上之后最重要的是会看病。这一章按我真实的排查路径来讲从全局看到细节定位中间穿插真实案例。3.1 dashboard一屏概览决定下一步往哪儿走进入Arthas后我几乎第一件事就是敲dashboard。界面信息量很大线程数、内存各区域使用率、GC情况、CPU占用Top线程。老年代的上涨速度是我最关注的指标。如果老年代每隔几分钟就明显增长通常有两种情况一是大对象持续产生并被晋升二是某类对象被长期引用无法回收。前者往往伴随Young GC后大量对象存活后者大概率指向缓存放飞或者ThreadLocal未清理。dashboard本身不解决问题但它帮你圈定方向——下一步是看GC细节还是导出堆快照在这个阶段基本已经能判断。CPU Top线程列表同样关键。某个线程长期占据高CPU时基础信息里能看到线程名再配合thread命令看细节。3.2 thread不只能看栈还能直接抓最忙的线程thread命令比jstack好用太多的地方在于支持直接输出最繁忙的线程。CPU突然飙高时thread -n 3立刻打印当前CPU占用最高的三个线程的栈帧。很多问题一眼就能定位Gson在toString一个巨大的对象、某个正则回溯爆炸、或者某个循环忘了退出条件。还有两个高频场景。线程死锁用thread -b检测并打印死锁线程栈高亮显示哪两个线程互相持有锁。线程池饥饿用thread --stateWAITING或--stateBLOCKED过滤线程状态如果大量业务线程都卡在WAITING又被同一个锁阻塞基本就是有人在持锁做慢操作。我之前遇到过一个案例某服务每隔一段时间就出现大面积超时日志里全是数据库连接获取超时。用thread -n抓线程栈发现大量线程都阻塞在DruidDataSource.getConnection()的锁上说明连接池被打满。再配合watch观察获取连接前后的耗时发现是某个慢SQL占着连接不释放。整个过程不到十分钟。3.3 watch把方法调用的现场录下来watch是最能体现Arthas价值的功能之一。典型的空指针场景订单服务大面积报错你怀疑是下游返回了null但日志里根本没打印入参。用watch把方法调用现场录下来watch com.example.OrderService createOrder {params, returnObj, throwExp} -x 3-x 3表示展开参数的深度为3层防止嵌套对象被截断。命令挂上后下一次目标方法被调用时Arthas会把入参、返回值、异常完整打出来不用重新部署不用加临时日志。更高级的用法是条件过滤只观察你关心的特定调用watch com.example.OrderService createOrder {throwExp} -x 2 params[0].userId 123456只有userId等于123456的调用才会打印。线上流量大的接口这一步能把噪音滤掉大半。3.4 trace方法内部耗时分布慢接口定位利器trace解决这个接口到底慢在哪的问题trace com.example.OrderService createOrder #cost 200命令执行后只要有一次调用总耗时超过200msArthas就把该方法内部所有子调用的耗时分布打出来哪一行代码耗时多少毫秒一清二楚。我碰过的最典型的案例一个下单接口平缓时80ms高峰期涨到2秒。用trace观察后发现耗时不在本地逻辑而在一次Redis读取与下一次调用之间出现了异常间隔。深入一步用watch观察Redis返回值发现是网络重连导致SocketReadTimeout等待时间被计入了接口总耗时。这类问题靠日志分析几乎等于大海捞针但trace直接告诉你每一段的耗时指向性非常强。使用trace要注意性能成本。它会给目标方法加计时插桩大流量接口长时间挂着trace吞吐会有可见下降。线上使用我习惯加条件、设阈值用完立刻stop。3.5 jad反编译验证线上到底跑的是哪版代码排查时经常遇到我改的代码已经上线了为什么问题还在。用jad确认线上类的真实内容心里就有底了jad com.example.OrderService它会反编译JVM里实际加载的类是真正的运行时代码而不是Git仓库里我以为的版本。如果发现不一致可能的原因包括新版没发上去、缓存目录有旧jar、热部署工具加载了旧类。jad让我们在做判断时少了一层朦胧。4. 导出dump对象heapdump、classloader dump 与 core dump 三个容易混淆的出口4.1 heapdump导出堆快照的完整实操与性能评估heapdump是OOM排查的重头戏。Arthas里的命令heapdump /tmp/heap.hprof如果是只保留存活对象heapdump --live /tmp/heap-live.hprof这里有个关键点必须讲清楚heapdump默认导出全部堆对象包括不可达对象--live参数导出前会先触发一次Full GC把垃圾回收后再导出。生产环境堆压力大时贸然执行live参数很可能加剧GC停顿。我的做法是如果不急着分析垃圾对象默认参数导出全量堆如果必须分析当前存活对象为什么占用大量堆就在业务低峰期执行live。heapdump与jmap -dump的核心机制相似但Arthas的优势在于已经attach在进程上不需要另起命令还能边观察边导出。导出过程的影响主要取决于堆大小——堆越大导出越慢期间目标进程会有明显的STW。实测8G堆导出大约需要数秒几十G大堆会到分钟级别。这个操作务必放在低峰期并且提前和团队报备。导出完成的hprof文件是整个分析链路里最重要的产物后面会专门讲怎么用MAT做深度分析。4.2 classloader dumpMetaspace泄漏的关键证据如果你怀疑类加载器泄漏比如频繁热部署后Metaspace不断上涨heapdump往往看不出端倪因为类元数据并不都在堆内。这时用Arthas的classloader命令classloader它会列出所有类加载器及各自加载的类数量。数量异常膨胀、同一应用出现大量重复类加载器基本可以断定有类加载器泄漏。再进一步dump出某个类加载器加载到的资源classloader --dump-url classLoaderHash /tmp/classes/这在排查动态代理类爆炸、自定义ClassLoader重复创建时很有效。比起jmap -clstats只给出统计数字Arthas能直接把类的字节码导出来结合jad对比哪些类被重复加载了。这类问题在Spring Boot的DevTools或者不停热部署的容器里尤其常见。4.3 core dumpJava进程崩溃时操作系统的尸体以及fail to write core dump的排查core dump和前面两个完全不同。heapdump是JVM主动导出的堆快照core dump是操作系统在进程崩溃时生成的内存映像。Java进程崩溃JVM自身崩溃、本地方法Segmentation Fault时才会触发。生产环境经常遇到的报错是fail to write core dump或提示生成了但目录里找不到core文件。按优先级排查这几个点ulimit -c是否为0。如果是用ulimit -c unlimited放开但这只对当前shell有效永久生效要写/etc/security/limits.conf。cat /proc/sys/kernel/core_pattern。如果配置的是管道方式交给apport等工具处理工具自身失败会导致core丢失如果路径指向了不存在的目录core同样写不进去。磁盘空间和写权限。core文件可能非常大目标分区满了就写不进去。容器环境。容器里崩溃时core存储路径由宿主机配置决定没配置就会丢。SELinux/AppArmor限制了对特定目录的写操作。排查顺序是先看ulimit -c再看kernel.core_pattern再确认目录余量和权限最后可以主动用一个小程序触发崩溃验证整条链路是否通畅。core文件拿到之后怎么分析Linux下标准流程是gdb java core.12345 (gdb) btbacktrace能直接看到崩溃时的调用栈。Java层面异常时JVM会先打印hs_err_pid日志core更多用于定位JVM自身或本地代码的问题。AIX平台上类似的排查用dbxAIX Java体系里还有javacore、heapdump等IBM转储格式配合IBM的Thread Analyzer和Heap Analyzer分析。虽然这些平台现在不多见但如果遇到了至少这条技术路线是通的。4.4 导出后文件太大怎么办生产环境导出的hprof经常动辄几个Gscp到本地非常慢。我的经验是先在服务器上做gzip压缩hprof的压缩率很高通常能压到三分之一以下。压缩后再传输本地MAT也能直接读压缩格式。如果公司有对象存储或内网文件分享平台直接传上去再下载比跨机房scp稳定得多。5. 拿到dump文件之后用MAT快速定位内存泄漏的完整流程导出dump不是目的从dump里看懂问题才是目的。这一章讲我最常用的一条分析链路。5.1 Leak Suspects Report先让工具给出嫌疑名单Eclipse MAT是分析hprof最顺手的工具没有之一。打开hprof文件后先让它跑一遍Leak Suspects Report。MAT会自动找出最可疑的几个问题对象并给出引用链。看到报告后我通常会点进每个suspect关注两件事一是哪些对象实例数量异常多二是从GC Roots到这些对象的引用路径是什么。路径里如果出现某个静态字段、ThreadLocal或者缓存Map嫌疑就非常明确了——这不是靠猜而是实打实的引用链证据。5.2 Dominator Tree按保留堆排序逐个排查大对象Leak Suspects是机器筛出的第一轮嫌疑Dominator Tree则适合人工逐个过。它按保留堆Retained Heap排序保留堆越大说明这个对象一旦被回收能释放的内存越多。按保留堆排序后经常看到的是几个明显的异常某个ArrayList实例包含数十万个元素、某个HashMap的Value是巨大的String数组、某个线程的ThreadLocal里挂着整个Request上下文没清理。找到这类对象之后回到代码里判断它为什么没释放。我遇到过一个很典型的案例一个Spring Boot应用老年代从2G涨到6G不到一周。dump出来用MAT看Dominator Tree排在第一的是一个ConcurrentHashMapRetained Heap高达2.3G。顺着GC Roots路径点下去发现是某个全局缓存组件以请求URL为key缓存了每次请求的响应报文TTL设成了7天。这类看起来无害的缓存低并发时没事大促流量一进来value的大小和条数同时膨胀内存直接失控。这种结论不dump根本不可能靠猜出来。5.3 分析前的准备别让MAT自己先OOM几十G的dump文件MAT默认的1G内存配置会直接OOM。打开MAT安装目录下的MemoryAnalyzer.ini把-Xmx调大-Xmx8192m一般建议-Xmx设为dump文件大小的1.5到2倍以上。同时注意MAT解析大文件时会消耗大量CPU和内存不建议在生产服务器上直接跑本地工作站更合适。还有一个跨JDK版本的坑不同JDK版本的dump格式有差异旧版MAT打不开新JDK生成的dump。遇到版本不兼容优先升级到最新版MAT如果还不行可以用VisualVM或JProfiler做补充不过论分析细节和速度MAT依然是首选。6. 生产环境落地Arthas的几条实用建议工具本身很强但如果用得太糙反而会增加故障。最后聊聊落地层面的几条经验。6.1 权限与审计Arthas能看到的敏感数据比你想象的要多Arthas可以watch到任何方法的入参和返回值这意味着它能直接看到用户手机号、token、支付信息等敏感数据。所以权限管理不能马虎。诊断权限单独授予操作留痕统一走堡垒机入口。目标进程上的Telnet直连端口建议关闭只允许安全通道访问。连接时注意绑定内网IP避免暴露到外网。6.2 性能开销控制诊断命令别在生产挂一天字节码增强不是免费的。watch命令会在每次调用目标方法时执行表达式求值trace会给方法内部每一段都插桩。一个每秒上万次调用的核心服务如果watch命令挂一整天不下来吞吐下降几个百分点是必然的。我的约定是三点生产环境watch和trace必须带条件或阈值只观察指定参数或超过阈值的调用定位完成立即stop不保留现场批处理采集时限制次数比如dashboard -n 3只打印三次。如果团队规模大可以考虑用Arthas Tunnel统一管理诊断会话远程诊断的权限和审计都会更规范。6.3 高危命令的边界reset、redefine这类操作要谨慎Arthas有几个高危命令非必要不碰。比如reset会撤销所有字节码增强redefine能热替换类字节码。这类操作既能解除故障也能制造更大的故障——redefine之后如果新代码有问题类的状态会处于一种半热替换的暧昧状态有时候需要重启才恢复。生产环境除非事故处理流程里明确批准了热修复方案否则不要碰。6.4 把团队诊断经验沉淀成手册Arthas的学习曲线其实不陡团队落地还是靠实战沉淀。建议把线上出过的每类问题按现象—定位命令—结论整理成手册新同学接手时照着跑一遍就能排查大部分问题。大概的样子接口变慢dashboard看GC → thread -n看线程 → trace定位方法链路内存上涨dashboard看老年代 → heapdump导出 → MAT分析CPU飙高thread -n 3抓热点线程 → watch对应方法入参和循环逻辑每个团队遇到的问题都不一样但沉淀这类手册的收益是复利式的。再分享一个我个人的习惯每次排查完线上问题把当时的Arthas命令、截图、dump分析结果完整保存下来写几行结论。下次遇到类似问题翻记录定位时间能从小时级压缩到分钟级。这也是我最想传达的一个观点Arthas不是万能的但它配合一套靠谱的诊断流程可以让你在面对线上故障时不再靠运气和直觉。