研发中心

死磕JVM | 用Arthas排查JVM内存 真爽!

发布日期:2022-08-07 13:33    点击次数:53

本文转载自微信公众号「牧小农」,作者牧小农。转载本文请联系牧小农公众号。

Arthas是啥

当我们系统遇到JVM或者内存溢出等问题的时候,如何对我们的程序进行有效的监控和排查,就发现了几个比较常用的工具,比如JDK自带的 jconsole、jvisualvm还有一个最好用的工具——jprofiler,但是这个是收费的,或者除了很有钱的公司,一般很少人会用这个,还有一个就是我们今天的主角——Arthas ,为什么今天会重点讲这个呢?

官网地址:http://arthas.gitee.io/

GitHub地址:https://github.com/alibaba/arthas/

Arthas 是Alibaba开源的Java诊断工具,采用命令行交互模式,提供了较为丰富的功能,主要还是他是免费里面的算是好用且功能比较强大的一个JVM排查的插件,在了解这个利器之后,发现还是挺好用的,而且支持的功能也比较全面,那么Arthas到底可以为我们做哪些事情呢?

1.提供性能看板,包括线程、cpu、内存等信息,并且会定时的刷新。

2.根据各种条件查看线程快照。找出cpu占用率最高的n个线程

3.输出jvm的各种信息,如gc算法、jdk版本、ClassPath等

4.遇到问题无法在线上 debug,热部署加日志直接替换

5.查看某个类的静态属性,也可以通过ognl语法执行一些语句

6.查看已加载的类的详细信息,这个类从哪个jar包加载的,查看类的方法的信息

7.dump 类的字节码到指定目录

8.直接反编译指定的类

9.快速定位应用的热点,生成火焰图

10.可以监控到JVM的实时运行状态

以前,你碰到这些问题,解决的办法大多是,修改代码,重新上线。但是在大公司里,上线的流程是非常繁琐的,如果为了多加一行日志而重新发布版本,无疑是非常折腾人的。但是阿里巴巴开源的Arthas 有了更为优雅的线上调试方法。

Arthas 支持JDK6,同时可以在 Linux/Mac/Windows上运行,自动Tab 补全功能,更方便我们定位问题和诊断

下载地址:https://arthas.gitee.io/download.html 你可以下载zip的包我下载的是arthas-packaging-3.5.0-bin.zip 或者通过命令去下载

wget https://alibaba.github.io/arthas/arthas-boot.jar

使用手册

1. 快速启动

当我们下载好之后,我们直接通过命令启动就可以java -jar arthas-boot.jar,但是在此之前我们需要通过检测的代码来挂靠到Arthas上面

import java.math.BigDecimal; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit;  public class FullGCTest {       //模拟银行卡的类     private static class CardInfo {         //小农的银行卡信息记录         BigDecimal price = new BigDecimal(10000000.0);         String name = "牧小农";         int age = 18;         Date birthdate = new Date();          public void m() {}     }      //线程池 定时线程池     //50个,然后设置 拒绝策略     private static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(50,             new ThreadPoolExecutor.DiscardOldestPolicy());      public static void main(String[] args) throws Exception {         executor.setMaximumPoolSize(50);          for (;;){             modelFit();             Thread.sleep(100);         }     }      /**      * 对银行卡进行风险评估      */     private static void modelFit(){         List<CardInfo> taskList = getAllCardInfo();         //拿出每一个信息出来         taskList.forEach(info -> {             // do something             executor.scheduleWithFixedDelay(() -> {                 //调用M方法                 info.m();              }, 2, 3, TimeUnit.SECONDS);         });     }      private static List<CardInfo> getAllCardInfo(){         List<CardInfo> taskList = new ArrayList<>();         //每次查询100张卡出来         for (int i = 0; i < 100; i++) {             CardInfo ci = new CardInfo();             taskList.add(ci);         }          return taskList;     } } 

这个是上篇文章讲述的案例,感兴趣的可以了解一下。

首先我们需要使用javac 命令将Java文件进行编译javac FullGCTest.java进行编译,然后打印GC日志,进行风险监控打印GC日志:

java -Xms200M -Xmx200M -XX:+PrintGC FullGCTest 

Arthas启动命令:java -jar arthas-boot.jar,get一下

我们就看到了我们刚才启动的FullGCTest的应用程序,我们输入编号 1 回车,这样我们就把Arthas挂靠到我们的程序上,接下来我们只需要做对应的命令操作就可以了

命令详情文档:https://arthas.aliyun.com/doc/commands.html

2. 功能列表

命令 详细说明 jvm 查看当前JVM信息 thread 查看当前JVM的线程堆栈信息 watch 方法执行数据观测 dashboard 当前系统的实时数据面板 trace 方法内部调用路径,并输出方法路径上的每个节点上耗时 stack 输出当前方法被调用的调用路径 tt 方法执行数据的时空隧道,巨婴心理记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测 vmoption 查看,更新JVM已加载的类信息 sc 查看JVM已加载的类信息 sm 查看已加载类的方法信息 jad 反编译指定已加载类的源码 classloader 查看classloader的继承树,urls,类加载信息 heapdump 类似jmap命令的heap dump 功能

jvm

OPERATING-SYSTEM:系统相关参数

THREAD相关:

COUNT : JVM当前活跃的线程数 DAEMON-COUNT : JVM当前活跃的守护线程数 PEAK-COUNT: 从JVM启动开始曾经活着的最大线程数 STARTED-COUNT: 从JVM启动开始总共启动过的线程次数 DEADLOCK-COUNT: JVM当前死锁的线程数

FILE-DESCRIPTOR(文件描述符相关):

MAX-FILE-DESCRIPTOR-COUNT:JVM进程最大可以打开的文件描述符数 OPEN-FILE-DESCRIPTOR-COUNT:JVM当前打开的文件描述符数 thread 命令

参数说明:

命令 详细说明 id 线程id [n:] 指定最忙的前N个线程并打印堆栈 [b] 找出当前阻塞其他线程的线程 [i] 指定cpu使用率统计的采样间隔,单位为毫秒,默认值为200 [--all] 显示所有匹配的线程

打印当前最忙的N个线程并打印堆栈

thread -n 3 

thread 查看所有线程

thread 17:显示指定线程的运行堆栈

thread -i: 指定采样时间间隔

thread -i 1000 : 统计最近1000ms内的线程CPU时间。thread -n 3 -i 1000 : 列出1000ms内最忙的3个线程栈

dashboard 命令

运行程序时,会显示当前程序的实时信息,如qps, rt, 错误数, 线程池信息等等

数据说明:

ID: Java级别的线程ID NAME: 线程名 GROUP: 线程组名 PRIORITY: 线程优先级, 1~10之间的数字,越大表示优先级越高 STATE: 线程的状态CPU%: 线程的cpu使用率。比如采样间隔1000ms,某个线程的增量cpu时间为100ms,则cpu使用率=100/1000=10% DELTA_TIME: 上次采样之后线程运行增量CPU时间,数据格式为秒 TIME: 线程运行总CPU时间,数据格式为分:秒 ?NTERRUPTED: 线程当前的中断位状态 DAEMON: 是否是daemon线程

参数说明:

参数名称 详细说明 id 刷新实时数据的时间间隔 (ms),默认5000ms [n:] 刷新实时数据的次数

sc 命令

查看JVM已加载的类信息,通过SC我们可以看到我们这个类的详细信息,包括是从哪个jar包读取的,他是不是接口/枚举类等,甚至包括他是从哪个类加载器加载的。

参数说明:

参数名称 详细说明 class-pattern 类名表达式匹配 method-pattern 方法名表达式匹配 [d] 输出当前类的详细信息,包括这个类所加载的原始文件来源、类的声明、加载的ClassLoader等详细信息。如果一个类被多个ClassLoader所加载,则会出现多次 [E] 开启正则表达式匹配,默认为通配符匹配

sc -d *CardInfo:打印类的详细信息

sc -d -f *CardInfo:打印类的Fiedld信息

heapdump + jhat分析

heapdump:类似于jmap命令

创建到指定文件夹下:

[arthas@365564]$ heapdump /usr/local/mxn/dump.hprof Dumping heap to /usr/local/mxn/dump.hprof ... Heap dump file created 

创建成功后,我们就可以在指定文件夹下看到对应的dump文件,然后使用命令jhat dump.hprof,生成文件,成功后我们就可以通过IP+端口进行访问了

访问:

然后我们就可以通过IP+端口去访问它了,里面有个他的other,我们拉到最底下,找

Show instance counts for all classes (including platform)

从下面我们可以分析出来哪个类包含的对象最多,分析出来哪个类产生的对象

这个里面最强大的功能还是叫做 Execute Object Query Language (OQL) query,这个里面可以显示有哪些对象,对象有多少个字节和引用,可以观察到哪个对象产生了问题,如下图所示,显示所有String对应的对象

搜索点进去之后我们还能看到这个对象到底占用了多少个字节,有多少个引用指向了这个Object,这个OQL的语法也是很灵活,我们可以使用where条件去过滤

jad

jad:反编译某个类,或者反编译某个类的某个方法,动态代理生成类的问题定位 第三方的类(观察代码) 版本问题(确定自己最新提交的版本是不是被使用)

有人可能会问这个有啥用,源码我不是自己就知道吗?因为有时我们经常会不确定线上或者测试环境的包是否是我们修改过的,这时候就可以通过jad反编译来看下,是否是最新的代码

redafine

redafine:热替换,动态更新代码,不用重启jvm目前有些限制条件:只能改方法实现(方法已经运行完成),不能改方法名, 不能改属性 m() -> mm()

比如我们在线上环境有个class确认有问题,想要重新替换,一般情况下只能停掉服务器重新发布,在普通的小公司这样是可以的,但是在大规模公司京东淘宝这样的是不能停的,因为整个流程是非常复杂的,那怎么办呢?大家可以看到下面的案例

首先我们新建一个测试案例:

public class T{     public static void main(String[] args) throws Exception{                     for(;;){                     System.in.read();                     new TT().m();                 }         } } 
public class TT{         public void m(){         System.out.println(2);     } } 

使用命令javac *.java,编译成class文件,然后运行 T 文件

[root@VM-0-7-centos t]# java T a 2 2 

当我们输入a的时候打印2,但是我们上线以后才发现,我们需要输出的1,这个是如果要从本地更改要重新发布上线,为了这一个修改,明显是不值当的,但是如果我们用 redafine 热部署就可以帮助我们直接替换,不用重新发布jvm

然后我们将 T 这个程序挂靠到 Arthas 上面去

然后我们直接修改 TT.java 程序 vi TT.java,将里面打印2的值修改成1

public class TT{         public void m(){             System.out.println(1);         } } 

然后编译执行 javac TT.java文件

在回到我们挂靠的Arthas 上面执行 redefine /usr/local/mxn/fuccGc/t/TT.class文件

[arthas@398842]$ redefine /usr/local/mxn/fuccGc/t/TT.class redefine success, size: 1, classes: TT 

执行成功 大家可以看到我们在没有重新启动的情况下成功替换了class文件图片

watch

watch:方法执行的数据观测,可以通过watch指令,来监控某个类,监控后,运行下你的功能,复现下场景,arthas会提供给你具体的出参和入参,帮助你排查故障

trace

输出方法调用路径,并输出耗时,这个指令对于优化代码非常的有用,可以看出具体每个方法执行的时间,如果是for循环等重复语句,还能看出n次循环中的最大耗时,最小耗时,和平均耗时,完美!

tt

在我们对某个方法开启tt后,会记录每一次调用(我们可以设置最大监控次数)的入参和返回参数,并能对这些不同时间下调进行观测

[arthas@405136]$ tt -t FullGCTest modelFit 

命令参数解析-t tt 命令有很多个主参数,-t 就是其中之一。这个参数的表明希望记录下类 *Test 的 print 方法的每次执行情况。-n 3 当你执行一个调用量不高的方法时可能你还能有足够的时间用 CTRL+C 中断 tt 命令记录的过程,但如果遇到调用量非常大的方法,瞬间就能将你的 JVM 内存撑爆。

此时你可以通过 -n 参数指定你需要记录的次数,当达到记录次数时 Arthas 会主动中断tt命令的记录过程,避免人工操作无法停止的情况。

ognl表达式

ognl表达式

OGNL特殊用法请参考:https://github.com/alibaba/arthas/issues/71 OGNL表达式官方指南:https://commons.apache.org/proper/commons-ognl/language-guide.html

调用静态函数:ognl '@java.lang.System@out.println("hello")'方法 获取静态类的静态字段:ognl '@FullGCTest@random'方法

Arthas还支持Web Console,详见:https://alibaba.github.io/arthas/web-console.html

总结

 

Arthas是一个线上Debug神器,相比于其他工具,Arthas有着比较全面的功能,上手也比较容易,对于刚开始入门的小伙伴也是可以轻松掌握的,对于文中有不懂或者有问题的小伙伴,大家可以在下面留言评论。

 



上一篇:听说 JVM 性能优化很难?今天我小试了一把!
下一篇:内存管理两部曲之虚拟内存管理