深入学习JVM(6): Jvm调优工具-Arthas

 2023-01-29
原文作者:code养牧人 原文地址:https://juejin.cn/post/6948812808171552804

一. 前言

今天说的是阿里开源的Jvm调优工具Arthas, 这个工具的强大之处应该都有所耳闻了. 它也不仅仅只是一个调优工具, 有非常多强大的功能. 比如热加载类, 动态更新内存变量等等.

二. Jdk自带的调优命令

说起调优工具, Jdk自带的命令也必不可少, 不过这里只简单的记录一下, 不是本文的重点. 因为真正在观察Jvm运行情况的时候, Arthas要方便的多.

  1. 查看当前有哪些java程序正在运行: jps
  2. 查看Jvm内存情况命令: jmap ① 查看内存占用情况及实例个数: jmap -histo 进程id 可以选择输入到文件(jmap -histo 进程id > test.txt)后再打开查看.
    ② 查看堆信息: jmap -heap 进程id
    ③ 导出dump文件: jmap ‐dump:format=b,file=test.hprof 进程id format: 格式化, file: 输出文件. 可以通过jvisualvm打开, jvisualvm也是jdk自带的工具(图形化的), 如果有配置jdk环境变量的话, 可以在windows的cmd上直接输入jvisualvm打开.
    ④ 除了主动导出dump文件之外, 也可以配置如下Jvm参数导出:
    -XX:+HeapDumpOnOutOfMemoryError
    -XX:HeapDumpPath=./
    配置之后, Jvm会在堆内存溢出时, 导出dump文件, 方便后续排查导致内存溢出的故障
  3. 查看Jvm运行信息: jinfo ① 启动参数: jinfo -flag 进程id
    ② 运行环境信息: jinfo -sysprops 进程id
  4. 查看Jvm堆栈信息: jstack
    ① 查找死锁或查看线程运行情况: jstack 进程id
    ② 查看某一个线程的堆栈信息, 此处列出查找占用CPU最高的线程:
    1. top -p 进程id           // 列出指定进程的信息
    
    2. 按下 H (大写)           // 列出该进程的所有线程
    
    3. 将CPU占用最高的线程id转为16进制, 因为java的线程id是用16进制来表示的, 
    top把它转为了十进制展示. 如: 28071 = 6da7
    
    4. jstack 进程id | grep -A 20 6da7    //可以打印出该线程正在执行的近20行代码
    
  1. 查看垃圾收集统计: jstat
    jstat -gc 进程id 这个比较重要, 放个图:

202301011628354571.png

除了最后一个之外, 其它的没有截图, 请自行尝试. 上述的所有命令都在jdk/bin目录下, 并且都可以通过 -h 查看帮助, 如: jmap -h

三. Arthas工具

千呼万唤始出来, 终于到Arthas了. 前面的命令可以了解一下即可, 重点记住这个工具就行, 我也不会写的太多, 只会列出几个常用的命令, 因为官网的在线教程做的真是天衣无缝~

附上Arthas官网地址: arthas.gitee.io/

下面一起来看看常用的功能吧(仅记录, 不做初级使用教程, 入门请看官网.):

  1. 支持了一些基础命令, 如cat, pwd, grep, echo, history等, 了解Linux同学的应该会很清楚这些命令. 对了, 同样支持 “ > ”, 将输出的内容追加到某一个文件中. 还有一个session, 可以查看当前进入的java进程号.
  2. vmoption: 查看Jvm的诊断参数, 默认查看全部. (1) 可以通过加上参数名只看某一个, 如: vmoption PrintGCDetails
    (2) 更神奇的是可以通过此参数修改它: vmoption PrintGCDetails true
  3. thread: 查看Jvm中线程的资源占用情况
    (1) 可以通过 -n 参数查看指定个数最忙的线程: thread -n 3, 会直接打印出线程堆栈, 不必再像之前原生的Jvm参数jstack那样找来找去最后还要进行进制转换了. 也可以通过thread -n 3 -i 1000查看1秒中最忙的3个线程
    (2) 而直接使用thread命令, 会打开一个类似top命令一样的界面, 不同的是它展示的是当前java进程的线程占用资源情况.
    (3) thread -b查看死锁情况或锁阻塞情况. 或者通过thread --state WAITING命令查看指定状态的线程, 阻塞的状态是: BLOCKED
  4. 强大的ognl命令. 通过Arthas的ognl命令可以动态执行java代码, 或者修改一些变量的值. 这是一个非常厉害的特性, 应用场景...在下没有想到, 反正当代码发布到正式环境之后, 一切都显得阻碍重重.
    (1) 动态执行代码: ognl '@java.lang.System@out.println("hello ognl")'调用System类的out.println方法. 意思是, 使用类名全路径, 再写上自己的方法名, 就可以调用了.
    (2) 查看静态变量的值:
    ognl -c 类加载器hash @com.example.demo.arthas.user.UserController@logger; 这个命令需要加上类的类加载器hash值, 所以需要先使用sc -d 类路径查找出加载该类的类加载器, 如: sc -d com.example.demo.arthas.user.UserController | grep classLoaderHash
    可以加上 -x 参数指定需要展开的层数(当一个静态变量为对象, 且又包含多个对象时使用)
    (3) ognl表达式, 官方例子:
    ognl '#value1=@System@getProperty("java.home"), #value2=@System@getProperty("java.runtime.name"), {#value1, #value2}'
    通过表达式, 声明value1和value2两个临时变量并赋值, 返回一个List. 更多的我没有去了解, 因为没有用到, 估计学了也忘的快.
  5. 查看Jvm信息: Jvm, 可以查看到堆内存的分配, 以及gc情况(包括gc算法)
  6. dump堆内存信息: heapdump /tmp/dump.hprof
  7. 查看Jvm监控面板: dashboard, 可以看到线程的资源占用情况, 以及堆内存占用情况和GC情况.
  8. 快速查看方法运行是否正常: tt -t -n 10 demo.MathGame primeFactors -n 10 : 打印10次
  9. 查看某个方法的执行所消耗的时间: trace demo.MathGame run -n 10如果需要过滤可以加上表达式: trace demo.MathGame run -n 10 '#cost > 10'; 如果想要观察多个类的多个方法的执行(一般是排查调用链), 可以使用表达式: trace -E com.test.ClassA|org.test.ClassB method1|method2|method3

四. Arthas热更新类

这个用的最多, 单独拎出来写. 热更新一个类有多种做法:

  • 方式一: 直接在本地将类编译好, 上传到服务器上, 然后进入Arthas执行命令: redefine /tmp/TestDemo.class即可.
    (1) 不过redefine之后的类是不能复原的, 这意味着...如果你为了改一个bug, 却出现了两个bug...需要再次将本地代码还原, 编译, 上传, 然后再次redefine才行.
    (2) 又不过好在Arthas提供了另一个命令: retransform /tmp/TestDemo.class, retransform热加载的类就能够还原.
    (3) 通过retransform -l可以看到通过retranform命令加载的类列表
    (4) 通过retransform -d id移除指定的热加载进来的类. 此处的id是通过上一条命令查询到的. 也可以通过retransform --deleteAll删除所有热加载进来的类.
    (5) 注意! 执行上方移除后, 热加载的类仍在Jvm内存中, 需要重新执行一次retransform命令: retransform --classPattern com.test.TestDemo, 这样才能还原.
    (6) PS: 我个人还是比较偏向redefine, 本地做好测试即可.
    还有!!! 不论redefine还是retransform, 都不能热更新更改了方法签名或类成员变量的类. 方法签名: 方法的作用域, 返回值, 方法名, 方法参数
  • 方式二: 官网的标准流程, 中间步骤可根据情况省略: (1) jad --source-only com.TestDemo > /tmp/TestDemo.java 将要热更新的类反编译至一个java文件中
    (2) 修改该文件中的java代码
    (3) sc -d *UserController | grep classLoaderHash 找到该类的类加载器的hash值, 一般打印如下: classLoaderHash 1be6f5c3
    (4) mc -c 1be6f5c3 /tmp/TestDemo.java -d /tmp 将修改好的java文件编译到指定目录. -c 指定类加载器; -d 指定编译目录
    (5) 使用redefineretransform命令热更新该类

全文结束, 不过还是希望小伙伴儿们去官网在线尝试一下, 绝对受益匪浅. 然后也欢迎指正~