Arthas线上分析诊断调优工具以前我们要排查线上问题通常使用的是jdk自带的调优工具和命令。最常见的就是dump线上日志然后下载到本地导入到jvisualvm工具中。这样操作有诸多不变现在阿里团队开发的Arhtas工具拥有非常强大的功能并且都是线上的刚需尤其是情况紧急不方便立刻发版适合临时处理危急情况使用。下面分两部分来研究JVM性能调优工具1.JDK自带的性能调优工具虽然有了Arthas但也不要忘记JDK自带的性能调优工具在某些场景下他还是有很大作用的。而且Arthas里面很多功能其根本就是封装了JDK自带的这些调优命令。2.Arthas线上分析工具的使用这一部分主要介绍几个排查线上问题常用的方法。功能真的很强大刚兴趣的猿媛可以研究其基本原理。之前跟我同事讨论感觉这就像病毒一样可以修改内存里的东西真的还是挺强大的。以上两种方式排查线上问题没有优劣之分如果线上不能安装Arthas就是jdk自带命令如果jdk自带命令不能满足部分要求又可以安装Arthas那就使用Arthas。他们只是排查问题的工具重要的是排查问题的思路。不管黑猫、白猫能抓住耗子就是好猫。一、JDK自带的调优工具这里不是流水一样的介绍功能怎么用就说说线上遇到的问题我们通常怎么排查排查的几种情况。内存溢出出现OutOfMemoryError这个问题如何排查CPU使用猛增这个问题如何排查进程有死锁这个问题如何排查JVM参数调优下面来一个一个解决1、处理内存溢出报OutOfMemoryError错误第一步通过jmap -histo命令查看系统内存使用情况使用的命令jmap -histo 进程号运行结果num #instances #bytes class name ---------------------------------------------- 1: 1101980 372161752 [B 2: 551394 186807240 [Ljava.lang.Object; 3: 1235341 181685128 [C 4: 76692 170306096 [I 5: 459168 14693376 java.util.concurrent.locks.AbstractQueuedSynchronizer$Node 6: 543699 13048776 java.lang.String 7: 497636 11943264 java.util.ArrayList 8: 124271 10935848 java.lang.reflect.Method 9: 348582 7057632 [Ljava.lang.Class; 10: 186244 5959808 java.util.concurrent.ConcurrentHashMap$Node 8671: 1 16 zipkin2.reporter.Reporter$1 8672: 1 16 zipkin2.reporter.Reporter$2 Total 8601492 923719424 num序号 instances实例数量 bytes占用空间大小 class name类名称[C is a char[][S is a short[][I is a int[][B is a byte[][[I is a int[][]通过这个命令我们可以看出当前哪个对象最消耗内存。上面这个运行结果是我启动了本地的一个项目然后运行【jmap -histro 进程号】运行出来的结果直接去了其中的一部分。通过这里我们可以看看大的实例对象中有没有我们自定义的实例对象。通过这个可以排查出哪个实例对象引起的内存溢出。除此之外Total汇总数据可以看出当前一共有多少个对象暂用了多大内存空间。这里是有约860w个对象占用约923M的空间。第二步分析内存溢出查看堆空间占用情况使用命令jhsdb jmap --heap --pid 进程号比如我本地启动了一个项目想要查看这个项目的内存占用情况:[rootiZ2pl8Z ~]# jhsdb jmap --heap --pid 28692 Attaching to process ID 28692, please wait... Debugger attached successfully. Server compiler detected. JVM version is 11.0.1310-LTS-370 using thread-local object allocation. Garbage-First (G1) GC with 4 thread(s) Heap Configuration: MinHeapFreeRatio 40 MaxHeapFreeRatio 70 MaxHeapSize 2065694720 (1970.0MB) NewSize 1363144 (1.2999954223632812MB) MaxNewSize 1239416832 (1182.0MB) OldSize 5452592 (5.1999969482421875MB) NewRatio 2 SurvivorRatio 8 MetaspaceSize 21807104 (20.796875MB) CompressedClassSpaceSize 1073741824 (1024.0MB) MaxMetaspaceSize 17592186044415 MB G1HeapRegionSize 1048576 (1.0MB) Heap Usage: G1 Heap: regions 1970 capacity 2065694720 (1970.0MB) used 467303384 (445.65523529052734MB) free 1598391336 (1524.3447647094727MB) 22.622093161955704% used G1 Young Generation: Eden Space: regions 263 capacity 464519168 (443.0MB) used 275775488 (263.0MB) free 188743680 (180.0MB) 59.36794582392776% used Survivor Space: regions 6 capacity 6291456 (6.0MB) used 6291456 (6.0MB) free 0 (0.0MB) 100.0% used G1 Old Generation: regions 179 capacity 275775488 (263.0MB) used 186285016 (177.65523529052734MB) free 89490472 (85.34476470947266MB) 67.54951912187352% used下面来看看参数的含义堆空间配置信息Heap Configuration: /** * 空闲堆空间的最小百分比计算公式为HeapFreeRatio (CurrentFreeHeapSize/CurrentTotalHeapSize) * 100值的区间为0 * 到100默认值为 40。如果HeapFreeRatio MinHeapFreeRatio则需要进行堆扩容扩容的时机应该在每次垃圾回收之后。 */ MinHeapFreeRatio 40 /** * 空闲堆空间的最大百分比计算公式为HeapFreeRatio (CurrentFreeHeapSize/CurrentTotalHeapSize) * 100值的区间为0 * 到100默认值为 70。如果HeapFreeRatio MaxHeapFreeRatio则需要进行堆缩容缩容的时机应该在每次垃圾回收之后 */ MaxHeapFreeRatio 70 /**JVM 堆空间允许的最大值*/ MaxHeapSize 2065694720 (1970.0MB) /** JVM 新生代堆空间的默认值*/ NewSize 1363144 (1.2999954223632812MB) /** JVM 新生代堆空间允许的最大值 */ MaxNewSize 1239416832 (1182.0MB) /** JVM 老年代堆空间的默认值 */ OldSize 5452592 (5.1999969482421875MB) /** 新生代2个Survivor区和Eden区 与老年代不包括永久区的堆空间比值表示新生代老年代12*/ NewRatio 2 /** 两个Survivor区和Eden区的堆空间比值为 8表示 S0 S1 Eden 118 */ SurvivorRatio 8 /** JVM 元空间的默认值 */ MetaspaceSize 21807104 (20.796875MB) CompressedClassSpaceSize 1073741824 (1024.0MB) /** JVM 元空间允许的最大值 */ MaxMetaspaceSize 17592186044415 MB /** 在使用 G1 垃圾回收算法时JVM 会将 Heap 空间分隔为若干个 Region该参数用来指定每个 Region 空间的大小 */ G1HeapRegionSize 1048576 (1.0MB)G1堆使用情况Heap Usage: G1 Heap: regions 1970 capacity 2065694720 (1970.0MB) used 467303384 (445.65523529052734MB) free 1598391336 (1524.3447647094727MB) 22.622093161955704% used G1 的 Heap 使用情况该 Heap 包含 1970 个 Region结合上文每个 RegionSize1M因此 Capacity Regions * RegionSize 1970 * 1M 1970M已使用空间为 445.65M空闲空间为 1524.34M使用率为 22.62%。G1年轻代Eden区使用情况G1 Young Generation: Eden Space: regions 263 capacity 464519168 (443.0MB) used 275775488 (263.0MB) free 188743680 (180.0MB) 59.36794582392776% used G1 的 Eden 区的使用情况总共使用了 263 个 Region结合上文每个 RegionSize1M因此 Used Regions * RegionSize 263 * 1M 263MCapacity443M 表明当前 Eden 空间分配了 443 个 Region使用率为 59.37%。G1年轻代Survivor区使用情况和G1老年代使用情况和Eden区类似Survivor Space: regions 6 capacity 6291456 (6.0MB) used 6291456 (6.0MB) free 0 (0.0MB) 100.0% used G1 Old Generation: regions 179 capacity 275775488 (263.0MB) used 186285016 (177.65523529052734MB) free 89490472 (85.34476470947266MB) 67.54951912187352% used Survivor区使用情况和Eden区类似。 老年代参数含义和Eden区类似。通过上面的命令我们就能知道当前系统堆空间的使用情况了到底是老年代有问题还是新生代有问题。第三步导出dump内存溢出的文件导入到jvisualvm查看如果前两种方式还是没有排查出问题我们可以导出内存溢出的日志在导入客户端进行分析使用的命令是jmap -dump:filea.dump 进程号或者是直接设置JVM参数-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath./ 路径然后导入到jvisualvm中进行分析方法是点击文件-装入导入文件查看系统的运行情况了。通过分析实例数看看哪个对象实例占比最高这里重点看我们自定义的类然后分析这个对象里面有没有大对象从而找出引起内存溢出的根本原因。2、CPU使用猛增这个问题如何排查我们可以通过Jstack找出占用cpu最高的线程的堆栈信息下面来一步一步分析。假设我们有一段死循环不断执行方法调用线程始终运行不释放就会导致CPU飙高示例代码如下package com.lxl.jvm; public class Math { public static int initData 666; public static User user new User(); public User user1; public int compute() { int a 1; int b 2; int c (a b) * 10; return c; } public static void main(String[] args) { Math math new Math(); while(true){ math.compute(); } } }第一步运行代码使用top命令查看cpu占用情况如上现在有一个java进程cpu严重飙高了接下来如何处理呢第二步使用top -p 命令查看飙高进程top -p 46518我们看到了单独的46518这个线程的详细信息第三步按H获取每个线程的内存情况需要注意的是这里的H是大写的H。我们可以看出线程0和线程1线程号飙高。第四步找到内存和cpu占用最高的线程tid通过上图我们看到占用cpu资源最高的线程有两个线程号分别是40183624018363。我们一第一个为例说明如何查询这个线程是哪个线程以及这个线程的什么地方出现问题导致cpu飙高。第五步将线程tid转化为十六进制67187778是线程号为4013442的十六进制数。具体转换可以网上查询工具。第六步执行[ jstack 4018360|grep -A 10 67187778] 查询飙高线程的堆栈信息接下来查询飙高线程的堆栈信息jstack 4013440|grep -A 10 671908824013440表示的是进程号67187778 表示的是线程号对应的十六进制数通过这个方式可以查询到这个线程对应的堆栈信息从这里我们可以看出有问题的线程id是0x4cd0, 哪一句代码有问题呢Math类的22行。第七步查看对应的堆栈信息找出可能存在问题的代码上述方法定位问题已经很精确了接下来就是区代码里排查为什么会有问题了。备注上面的进程id可能没有对应上在测试的时候需要写对进程id和线程id3、进程有死锁这个问题如何排查Jstack可以用来查看堆栈使用情况以及进程死锁情况。下面就来看看如何排查进程死锁还是通过案例来分析package com.lxl.jvm; public class DeadLockTest { private static Object lock1 new Object(); private static Object lock2 new Object(); public static void main(String[] args) { new Thread(() - { synchronized (lock1) { try { System.out.println(thread1 begin); Thread.sleep(5000); } catch (InterruptedException e) { } synchronized (lock2) { System.out.println(thread1 end); } } }).start(); new Thread(() - { synchronized (lock2) { try { System.out.println(thread2 begin); Thread.sleep(5000); } catch (InterruptedException e) { } synchronized (lock1) { System.out.println(thread2 end); } } }).start(); } }上面是两把锁互相调用。定义了两个成员变量lock1lock2main方法中定义了两个线程。线程1内部使用的是同步执行--上锁锁是lock1。休眠5秒钟之后他要获取第二把锁执行第二段代码。线程2和线程1类似锁相反。问题一开始像个线程并行执行线程一获取lock1线程2获取lock2.然后线程1继续执行当休眠5s后获取开启第二个同步执行锁是lock2但这时候很可能线程2还没有执行完所以还没有释放lock2于是等待。线程2刚开始获取了lock2锁休眠五秒后要去获取lock1锁这时lock1锁还没释放于是等待。两个线程就处于相互等待中造成死锁。第一步通过Jstack命令来看看是否能检测到当前有死锁。jstack 51789从这里面个异常可以看出prio当前线程的优先级cpucpu耗时os_prio操作系统级别的优先级tid线程idnid系统内核的idstate当前的状态BLOCKED表示阻塞。通常正常的状态是Running我们看到Thread-0和Thread-1线程的状态都是BLOCKED.通过上面的信息我们判断出两个线程的状态都是BLOCKED可能有点问题然后继续往下看。我们从最后的一段可以看到这句话Found one Java-level deadlock; 意思是找到一个死锁。死锁的线程号是Thread-0Thread-1。Thread-0正在等待0x000000070e706ef8对象的锁这个对象现在被Thread-1持有。Thread-1正在等待0x000000070e705c98对象的锁这个对象现在正在被Thread-0持有。最下面展示的是死锁的堆栈信息。死锁可能发生在DeadLockTest的第17行和第31行。通过这个提示我们就可以找出死锁在哪里了。第二步使用jvisualvm查看死锁如果使用jstack感觉不太方便还可以使用jvisualvm通过界面来查看更加直观。在程序代码启动的过程中打开jvisualvm工具。找到当前运行的类查看线程就会看到最头上的一排红字检测到死锁。然后点击“线程Dump”按钮查看相信的线程死锁的信息。这里可以找到线程私锁的详细信息具体内容和上面使用Jstack命令查询的结果一样这里实用工具更加方便。4、JVM参数调优jvm调优通常使用的是Jstat命令。1. 垃圾回收统计 jstat -gcjstat -gc 进程id这个命令非常常用在线上有问题的时候可以通过这个命令来分析问题。下面我们来测试一下启动一个项目然后在终端驶入jstat -gc 进程id得到如下结果上面的参数分别是什么意思呢先识别参数的含义然后根据参数进行分析S0C: 第一个Survivor区的容量S1C: 第二个Survivor区的容量S0U: 第一个Survivor区已经使用的容量S1U:第二个Survivor区已经使用的容量EC: 新生代Eden区的容量EU: 新生代Eden区已经使用的容量OC: 老年代容量OU:老年代已经使用的容量MC: 方法区大小元空间MU: 方法区已经使用的大小CCSC:压缩指针占用空间CCSU:压缩指针已经使用的空间YGC: YoungGC已经发生的次数YGCT: 这一次YoungGC耗时FGC: Full GC发生的次数FGCT: Full GC耗时GCT: 总的GC耗时等于YGCTFGCT连续观察GC变化的命令jstat -gc 进程ID 间隔时间 打印次数举个例子我要打印10次gc信息每次间隔1秒jstat -gc 进程ID 1000 10这样就连续打印了10次gc的变化每次隔一秒。这个命令是对整体垃圾回收情况的统计下面将会差分处理。2.堆内存统计这个命令是打印堆内存的使用情况。jstat -gccapacity 进程IDNGCMN新生代最小容量NGCMX新生代最大容量NGC当前新生代容量S0C第一个Survivor区大小S1C第二个Survivor区大小ECEden区的大小OGCMN老年代最小容量OGCMX老年代最大容量OGC当前老年代大小OC: 当前老年代大小MCMN: 最小元数据容量MCMX最大元数据容量MC当前元数据空间大小CCSMN最小压缩类空间大小CCSMX最大压缩类空间大小CCSC当前压缩类空间大小YGC年轻代gc次数FGC老年代GC次数3.新生代垃圾回收统计命令jstat -gcnew 进程ID [ 间隔时间 打印次数]这个指的是当前某一次GC的内存情况S0C第一个Survivor的大小S1C第二个Survivor的大小S0U第一个Survivor已使用大小S1U第二个Survivor已使用大小TT: 对象在新生代存活的次数MTT: 对象在新生代存活的最大次数DSS: 期望的Survivor大小ECEden区的大小EUEden区的使用大小YGC年轻代垃圾回收次数YGCT年轻代垃圾回收消耗时间4. 新生代内存统计jstat -gcnewcapacity 进程ID参数含义NGCMN新生代最小容量NGCMX新生代最大容量NGC当前新生代容量S0CMXSurvivor 1区最大大小S0C当前Survivor 1区大小S1CMXSurvivor 2区最大大小S1C当前Survivor 2区大小ECMX最大Eden区大小EC当前Eden区大小YGC年轻代垃圾回收次数FGC老年代回收次数5. 老年代垃圾回收统计命令jstat -gcold 进程ID参数含义MC方法区大小MU方法区已使用大小CCSC:压缩指针类空间大小CCSU:压缩类空间已使用大小OC老年代大小OU老年代已使用大小YGC年轻代垃圾回收次数FGC老年代垃圾回收次数FGCT老年代垃圾回收消耗时间GCT垃圾回收消耗总时间新生代老年代6. 老年代内存统计命令jstat -gcoldcapacity 进程ID参数含义OGCMN老年代最小容量OGCMX老年代最大容量OGC当前老年代大小OC老年代大小YGC年轻代垃圾回收次数FGC老年代垃圾回收次数FGCT老年代垃圾回收消耗时间GCT垃圾回收消耗总时间7. 元数据空间统计命令jstat -gcmetacapacity 进程IDMCMN:最小元数据容量MCMX最大元数据容量MC当前元数据空间大小CCSMN最小指针压缩类空间大小CCSMX最大指针压缩类空间大小CCSC当前指针压缩类空间大小YGC年轻代垃圾回收次数FGC老年代垃圾回收次数FGCT老年代垃圾回收消耗时间GCT垃圾回收消耗总时间8.整体运行情况命令jstat -gcutil 进程IDS0Survivor 1区当前使用比例S1Survivor 2区当前使用比例EEden区使用比例O老年代使用比例M元数据区使用比例CCS指针压缩使用比例YGC年轻代垃圾回收次数YGCT年轻代垃圾回收消耗时间FGC老年代垃圾回收次数FGCT老年代垃圾回收消耗时间GCT垃圾回收消耗总时间通过查询上面的参数来分析整个堆空间。二、Arthas线上分析工具的使用Arthas的功能非常强大现附上官方文档https://arthas.aliyun.com/doc/其实想要了解Arthas看官方文档就可以了功能全而详细。那为什么还要整理一下呢我们这里整理的是一些常用功能以及在紧急情况下可以给我们帮大忙的功能。Arthas分为几个部分来研究先来看看我们的研究思路哈1.安装及启动---这一块简单看对于程序员来说so easy2.dashboard仪表盘功能---类似于JDK的jstat命令3.thread命令查询进行信息---类似于jmap命令4.反编译线上代码----这个功能很牛改完发版了怎么没生效反编译看看。5.查询某一个函数的返回值6.查询jvm信息并修改----当发生内存溢出是可以手动设置打印堆日志到文件7.profiler火焰图下面就来看看Arthas的常用功能的用法吧1、Arthas的安装及启动其实说到这快不得不提的是之前我一直因为arthas是一个软件要启动界面操作。当时我就想要是这样在线上安装一个单独的应用公司肯定不同意啊~~~研究完才发现原来Arthas就是一个jar包。运行起来就是用java -jar 就可以。1) 安装可以直接在Linux上通过命令下载wget https://alibaba.github.io/arthas/arthas-boot.jar也可以在浏览器直接访问https://alibaba.github.io/arthas/arthas-boot.jar等待下载成功后上传到Linux服务器上。2) 启动执行命令就可以启动了java -jar arthas-boot.jar启动成功可以看到如下界面然后找到你想监控的进程输入前面对应的编号就可以开启进行监控模式了。比如我要看4看到这个就表示进入应用监听成功2、dashboard仪表盘--查询整体项目运行情况执行命令dashboard这里面一共有三块1线程信息我们可以看到当前进程下所有的线程信息。其中第1314号线程当前处于BLOCKED阻塞状态阻塞时间也可以看到。通过这个一目了然当前有两个线程是有问题的处于阻塞状态GC线程有6个。2内存信息内存信息包含三个部分堆空间信息、非堆空间信息和GC垃圾收集信息堆空间信息g1_eden_space: Eden区空间使用情况g1_survivor_space: Survivor区空间使用情况g1_old_gen: Old老年代空间使用情况非堆空间信息codeheap_non-nmethods: 非方法代码堆大小metaspace: 元数据空间使用情况codeheap_profiled_nmethods:compressed_class_space: 压缩类空间使用情况GC垃圾收集信息gc.g1_young_generation.count新生代gc的数量gc.g1_young_generation.time(ms)新生代gc的耗时gc.g1_old_generation.count 老年代gc的数量gc.g1_old_generation.time(ms)老年代gc的耗时3 运行时信息os.name当前使用的操作系统 Mac OS Xos.version 操作系统的版本号 10.16java.versionjava版本号 11.0.2java.homejava根目录 /Library/Java/JavaVirtualMachines/jdk-11.0.2.jdk/Contents/Homesystemload.average系统cpu负载平均值4.43 load average值的含义 单核处理器 假设我们的系统是单CPU单内核的把它比喻成是一条单向马路把CPU任务比作汽车。当车不多的时候load 1当车占满整个 马路的时候 load1当马路都站满了而且马路外还堆满了汽车的时候load1Load 1Load 1Load 1 多核处理器 我们经常会发现服务器Load 1但是运行仍然不错那是因为服务器是多核处理器Multi-core。 假设我们服务器CPU是2核那么将意味我们拥有2条马路我们的Load 2时所有马路都跑满车辆。Load 2时马路都跑满了processors : 处理器个数 8timestamp/uptime采集的时间戳Fri Jan 07 11:36:12 CST 2022/2349s通过仪表盘我们能从整体了解当前线程的运行健康状况3.thread命令查询CPU使用率最高的线程及问题原因通过dashboard我们可以看到当前进程下运行的所有的线程。那么如果想要具体查看某一个线程的运行情况可以使用thread命令1. 统计cpu使用率最高的n个线程先来看看常用的参数。参数说明参数名称参数说明id线程id[n:]指定最忙的前N个线程并打印堆栈[b]找出当前阻塞其他线程的线程[ivalue]指定cpu使用率统计的采样间隔单位为毫秒默认值为200[--all]显示所有匹配的线程我们的目标是想要找出CPU使用率最高的n个线程。那么需要先明确如何计算出CPU使用率然后才能找到最高的。计算规则如下:首先、第一次采样获取所有线程的CPU时间(调用的是java.lang.management.ThreadMXBean#getThreadCpuTime()及sun.management.HotspotThreadMBean.getInternalThreadCpuTimes()接口) 然后、睡眠等待一个间隔时间默认为200ms可以通过-i指定间隔时间 再次、第二次采样获取所有线程的CPU时间对比两次采样数据计算出每个线程的增量CPU时间 线程CPU使用率 线程增量CPU时间 / 采样间隔时间 * 100% 注意 这个统计也会产生一定的开销JDK这个接口本身开销比较大因此会看到as的线程占用一定的百分比为了降低统计自身的开销带来的影响可以把采样间隔拉长一些比如5000毫秒。统计1秒内cpu使用率最高的n个线程:thread -n 3 -i 1000从线程的详情可以分析出目前第一个线程的使用率是最高的cpu占用了达到99.38%。第二行告诉我们是Arthas.java这个类的第38行导致的。由此我们可以一眼看出问题然后定位问题代码的位置接下来就是人工排查问题了。2、查询出当前被阻塞的线程命令thread -b可以看到内容提示线程Thread-1被线程Thread-0阻塞。对应的代码行数是DeadLockTest.java类的第31行。根据这个提示去查找代码问题。3、指定采样的时间间隔命令thread -i 1000这个的含义是个1s统计一次采样4.反编译线上代码----这个功能很牛改完发版了怎么没生效反编译看看。说道Arthas不得不提的一个功能就是线上反编译代码的功能。经常会发生的一种状况是线上有问题定位问题后立刻改代码可是发版后发现没生效不可能啊~~~刚刚提交成功了呀。于是重新发版只能靠运气不知道为啥没生效。反编译线上代码可以让我们一目了然知道代码带动部分是否生效。反编译代码使用Arthas的jad命令jad 命令将JVM中实际运行的class的byte code反编译成java代码用法jad com.lxl.jvm.DeadLockTest运行结果运行结果分析这里包含3个部分ClassLoader类加载器就是加载当前类的是哪一个类加载器Location:类在本地保存的位置源码类反编译字节码后的源码如果不想想是类加载信息和本地位置只想要查看类源码信息可以增加--source-only参数jad --source-only 类全名6. ognl 动态执行线上的代码能够调用线上的代码是不是很神奇了。感觉哪段代码执行有问题但是又没有日志就可以使用这个方法动态调用目标方法了。我们下面的案例都是基于这段代码执行User类public class User { private int id; private String name; public User() { } public User(int id, String name) { this.id id; this.name name; } public int getId() { return id; } public void setId(int id) { this.id id; } public String getName() { return name; } public void setName(String name) { this.name name; } }DeadLockTest类public class DeadLockTest { private static Object lock1 new Object(); private static Object lock2 new Object(); private static ListString names new ArrayList(); private ListString citys new ArrayList(); public static String add() { names.add(zhangsan); names.add(lisi); names.add(wangwu); names.add(zhaoliu); return 123456; } public ListString getCitys() { DeadLockTest deadLockTest new DeadLockTest(); deadLockTest.citys.add(北京); return deadLockTest.citys; } public static ListUser addUsers(Integer id, String name) { ListUser users new ArrayList(); User user new User(id, name); users.add(user); return users; } public static void main(String[] args) { new Thread(() - { synchronized (lock1) { try { System.out.println(thread1 begin); Thread.sleep(5000); } catch (InterruptedException e) { } synchronized (lock2) { System.out.println(thread1 end); } } }).start(); new Thread(() - { synchronized (lock2) { try { System.out.println(thread2 begin); Thread.sleep(5000); } catch (InterruptedException e) { } synchronized (lock1) { System.out.println(thread2 end); } } }).start(); } }1获取静态函数 返回值是字符串ognl 全路径类名静态方法名(参数)示例1在DeadLockTest类中有一个add静态方法我们来看看通过ognl怎么执行这个静态方法。执行命令ognl com.lxl.jvm.DeadLockTestadd() 其中第一个后面跟的是类的全名称第二个跟的是属性或者方法名如果属性是一个对象想要获取属性里面的属性或者方法直接打.属性名/方法名 即可。运行效果我们看到了这个对象的返回值是123456 返回值是对象ognl 全路径类名静态方法名(参数) -x 2这里我们可以尝试一下替换-x 2 为 -x 1 -x 3* 案例1返回对象的地址。不加 -x 或者是-x 1ognl com.lxl.jvm.DeadLockTestaddUsers(1,zhangsan) 或 ognl com.lxl.jvm.DeadLockTestaddUsers(1,zhangsan) -x 1返回值* 案例2返回对象中具体参数的值。加 -x 2ognl com.lxl.jvm.DeadLockTestaddUsers(1,zhangsan) -x 2返回值* 案例3返回对象中有其他对象命令ognl com.lxl.jvm.DeadLockTestaddUsers(1,zhangsan) -x 2执行结果-x 2 获取的是对象的值List返回的是数组信息数组长度。命令ognl com.lxl.jvm.DeadLockTestaddUsers(1,zhangsan) -x 3执行结果-x 3 打印出对象的值对象中List列表中的值。* 案例4方法A的返回值当做方法B的入参ognl #value1com.lxl.jvm.DeadLockTestgetCitys(), #value2com.lxl.jvm.DeadLockTestgeneratorUser(1,lisi,#value1), {#value1,#value2} -x 2 方法入参是简单类型的列表ognl com.lxl.jvm.DeadLockTestreturnCitys({beijing,shanghai,guangdong})方法入参是一个复杂对象ognl #value1new com.lxl.jvm.User(1,zhangsan),#value1.setName(aaa), #value1.setCitys({bj, sh}), #value2com.lxl.jvm.DeadLockTestaddUsers(#value1), #value2 -x 3 方法入参是一个map对象ognl #value1new com.lxl.jvm.User(1,zhangsan), #value1.setCitys({bj, sh}), #value2#{mum:zhangnvshi,dad:wangxiansheng}, #value1.setFamily(#value2), #value1 -x 22获取静态字段ognl 全路径类名静态属性名示例在DeadLockTest类中有一个names静态属性下面来看看如何获取这个静态属性。执行命令ognl com.lxl.jvm.DeadLockTestnames 其中第一个后面跟的是类的全名称第二个跟的是属性或者方法名如果属性是一个对象想要获取属性里面的属性或者方法直接打.属性名/方法名 即可。运行效果第一次执行获取属性命令返回的属性是一个空集合然后执行add方法往names集合中添加了属性再次请求names集合发现有4个属性返回。3 获取实例对象ognl #value1new com.lxl.jvm.User(1,zhangsan),#value1.setName(aaa), #value1.setCitys({bj, sh}), {#value1} -x 2获取实例对象使用new关键字执行结果7. 线上代码修改生产环境有时会遇到非常紧急的问题或突然发现一个bug这时候不方便重新发版或者发版未生效可以使用Arthas临时修改线上代码。通过Arthas修改的步骤如下1. 从读取.class文件 2. 编译成.java文件 3. 修改.java文件 4. 将修改后的.java文件编译成新的.class文件 5. 将新的.class文件通过classloader加载进JVM内第一步读取.class文件sc -d *DeadLockTest*使用sc命令查看JVM已加载的类信息。关于sc命令查看官方文档https://arthas.aliyun.com/doc/sc.html-d 表示打印类的详细信息最后一个参数classLoaderHash表示在jvm中类加载的hash值我们要获得的就是这个值。第二步使用jad命令将.class文件反编译为.java文件才行jad -c 7c53a9eb --source-only com.lxl.jvm.DeadLockTest /Users/lxl/Downloads/DeadLockTest.javajad命令是反编译指定已加载类的源码-c 类所属 ClassLoader 的 hashcode--source-only默认情况下反编译结果里会带有ClassLoader信息通过--source-only选项可以只打印源代码。com.lxl.jvm.DeadLockTest目标类的全路径/Users/lxl/Downloads/DeadLockTest.java反编译文件的保存路径/* * Decompiled with CFR. * * Could not load the following classes: * com.lxl.jvm.User */ package com.lxl.jvm; import com.lxl.jvm.User; import java.util.ArrayList; import java.util.List; public class DeadLockTest { private static Object lock1 new Object(); private static Object lock2 new Object(); private static ListString names new ArrayListString(); private ListString citys new ArrayListString(); public static ListString getCitys() { DeadLockTest deadLockTest new DeadLockTest(); /*25*/ deadLockTest.citys.add(北京); /*27*/ return deadLockTest.citys; } ...... public static void main(String[] args) { ...... } }这里截取了部分代码。第三步修改java文件public static ListString getCitys() { System.out.println(-----这里增加了一句日志打印-----); DeadLockTest deadLockTest new DeadLockTest(); /*25*/ deadLockTest.citys.add(北京); /*27*/ return deadLockTest.citys; }第四步使用mc命令将.java文件编译成.class文件mc -c 512ddf17 -d /Users/luoxiaoli/Downloads /Users/luoxiaoli/Downloads/DeadLockTest.javamc: 编译.java文件生.class文件 详细使用方法参考官方文档https://arthas.aliyun.com/doc/mc.html-c指定classloader的hash值-d指定输出目录最后一个参数是java文件路径这是反编译后的class字节码文件第五步使用redefine命令将.class文件重新加载进JVMredefine -c /Users/***/Downloads/com/lxl/jvm/DeadLockTest.class最后看到redefine success表示重新加载.class文件进JVM成功了。注意事项redefine命令使用之后再使用jad命令会使字节码重置恢复为未修改之前的样子。官方关于redefine命令的说明第六步检验效果这里检测效果调用接口执行日志即可。8、实时修改生产环境的日志级别这个功能也很好用通常我们在日志中打印的日志级别一般是infor、warn、error级别的debug日志一般看不到。那么出问题的时候一些日志在写代码的时候会被记录在debug日志中而此时日志级别又很高。那么迫切需要调整日志级别。这个功能很好用啊我们可以将平时不经常打印出来的日志设置为debug级别。设置线上日志打印级别为info。当线上有问题的时候可以将日志级别动态调整为debug。异常排查完在修改回info。这对访问量特别大日志内容很多的项目比较有效可以有效节省日志输出带来的开销。第一步使用logger命令查看日志级别当前应用的日志级别是info类加载的hash值是18b4aac2我们定义一个接口其源代码内容如下PostMapping(value test) public String test() { log.debug(这是一条 debug 级别的日志); log.info(这是一条 info 级别的日志); log.error(这是一条 error 级别的日志); log.warn(这是一条 warn 级别的日志); return 完成; }可以调用接口查看日志输出代码。我们看到日志输出的是info及以下的级别。第二步修改logger日志的级别logger -c 18b4aac2 --name ROOT --level debug修改完日志级别以后输出日志为debug级别。8. 查询jvm信息并修改----当发生内存溢出时可以手动设置打印堆日志到文件通常查询jvm参数使用的是Java自带的工具[jinfo 进程号]。arthas中通过vmoption获取jvm参数假设我们要设置JVM出现OutOfMemoryError的时候自动dump堆快照vmoption HeapDumpOnOutOfMemoryError true这时如果发生堆内存溢出会打印日志到文件9. 监控函数耗时trace 待监控方法的全类名 待监控的方法名trace com.lxl.jvm.DeadLockTest generatorUser通过圈起来的部分可以看到接口的入口函数time总耗时371ms其中getDataFromDb函数耗时200msgetDataFromRedis函数耗时100msgetDataFromOuter函数耗时50msprocess函数耗时20ms很明显最慢的函数已经找到了接下里就要去对代码进行进一步分析然后再进行优化