当前位置: 首页 > article >正文

JVM - JVM调优

JVM - JVM调优

文章目录

  • JVM - JVM调优
    • 一:JVM参数调优
      • 1:JVM选项规则
      • 2:JVM参数
        • 2.1:最常用的四个参数
        • 2.2:其他参数
      • 3:补充说明和生产经验
      • 4:垃圾回收
        • 4.1:垃圾回收算法
        • 4.2:GC考虑的指标
        • 4.3:回收器的JVM参数
    • 二:线程Dump分析
      • 1:thread Dump 抓取
      • 2:thread Dump 分析
        • 2.1:信息说明
        • 2.2:状态分析
      • 3:案例分析
      • 4:JVM重要线程
    • 三:生产环境下的JVM & GC配置建议
      • 1:JVM,GC配置建议
      • 2:OOM排查
        • 2.1:基本排查
      • 2:arthas分析工具
      • 3:arthas的其他使用说明
        • 3.1:类命令
        • 3.2:监测排查命令【最常用】
        • 3.3:JVM命令
        • 3.4:特殊命令
        • 3.5:帮助命令

一:JVM参数调优

1:JVM选项规则

  • java -version -> 标准选项,任何版本JVM、任何平台都可以使用
  • java -X -> 非标准选项,部分版本识别【其实都可以用】
  • java -XX -> 不稳定参数,不同JVM之间有差别,随时可能会被移除【+表示开启,-表示关闭】

2:JVM参数

2.1:最常用的四个参数

-Xms

堆最小值

-Xmx

堆最大堆值

-Xms与-Xmx 的单位默认字节都是以k、m做单位的。

通常这两个配置参数相等,避免每次空间不足,动态扩容带来的影响。减少内存的交换

-Xmn

新生代大小

-Xss

每个线程池的堆栈大小。

在jdk5以上的版本,每个线程堆栈大小为1m,jdk5以前的版本是每个线程池大小为256k。【建议改成256K

一般在相同物理内存下,如果减少-xss值会产生更大的线程数,但不同的操作系统对进程内线程数是有限制的,是不能无限生成

2.2:其他参数

-XX:NewRatio

设置新生代与老年代比值,-XX:NewRatio=4 表示新生代与老年代所占比例为1:4 ,新生代占比整个堆的五分之一。

如果设置了-Xmn的情况下,该参数是不需要在设置的

-XX:PermSize

设置持久代初始值,默认是物理内存的六十四分之一

-XX:MaxPermSize

设置持久代最大值,默认是物理内存的四分之一

-XX:MaxTenuringThreshold

新生代中对象存活次数,默认15。

若对象在eden区,经历一次MinorGC后还活着,则被移动到Survior区,年龄加1。以后,对象每次经历MinorGC,年龄都加1。达到阀值,则移入老年代

-XX:SurvivorRatio

Eden区与Subrvivor区大小的比值,如果设置为8,两个Subrvivor区与一个Eden区的比值为2:8,一个Survivor区占整个新生代的十分之一

-XX:+UseFastAccessorMethods

原始类型快速优化

-XX:+AggressiveOpts

编译速度加快

-XX:PretenureSizeThreshold

对象超过多大值时直接在老年代中分配

3:补充说明和生产经验

  • 整个堆大小的计算公式: JVM 堆大小 = 年轻代大小+年老代大小+持久代大小
  • 增大新生代大小就会减少对应的年老代大小,设置-Xmn值对系统性能影响较大,所以如果设置新生代大小的调整,则需要严格的测试调整。

  • 而新生代是用来存放新创建的对象,大小是随着堆大小增大和减少而有相应的变化,默认值是保持堆大小的十五分之一

  • -Xmn参数就是设置新生代的大小,也可以通过-XX:NewRatio来设置新生代与年老代的比例,java 官方推荐配置为3:8。

  • 新生代的特点就是内存中的对象更新速度快,在短时间内容易产生大量的无用对象,如果在这个参数时就需要考虑垃圾回收器设置参数也需要调整。

  • 推荐使用: 复制清除算法和并行收集器进行垃圾回收,而新生代的垃圾回收叫做初级回收。

StackOverflowError和OutOfMemoryException。

  • 当线程中的请求的栈的深度大于最大可用深度,就会抛出前者;若内存空间不够,无法创建新的线程,则会抛出后者。
  • 栈的大小直接决定了函数的调用最大深度,栈越大,函数嵌套可调用次数就越多

生产经验

  • -Xmn用于设置新生代的大小。过小会增加Minor GC频率,过大会减小老年代的大小。一般设为整个堆空间的1/4或1/3.

  • XX:SurvivorRatio用于设置新生代中survivor空间(from/to)和eden空间的大小比例;

  • XX:TargetSurvivorRatio表示,当经历Minor GC后,survivor空间占有量(百分比)超过它的时候,就会压缩进入老年代。默认值为50%。

  • 为了性能考虑,一开始尽量将新生代对象留在新生代,避免新生的大对象直接进入老年代。

  • 因为新生对象大部分都是短期的,这就造成了老年代的内存浪费,并且回收代价也高(Full GC发生在老年代和方法区Perm).

  • -Xms=-Xmx,可以使得堆相对稳定,避免不停震荡

一般来说,MaxPermSize设为64MB可以满足绝大多数的应用了。若依然溢出,则可以设为128MB。若128MB还不能满足需求,那么就应该考虑程序优化了,减少动态类的产生

4:垃圾回收

4.1:垃圾回收算法
  • 引用计数法: 会有循环引用的问题,古老的方法;

  • Mark-Sweep: 标记清除。根可达判断,最大的问题是空间碎片(清除垃圾之后剩下不连续的内存空间);

  • Copying: 复制算法。对于短命对象来说有用,否则需要复制大量的对象,效率低下

  • Mark-Compact: 标记整理。对于老年对象来说有用,无需复制,不会产生内存碎片

4.2:GC考虑的指标

吞吐量: 应用耗时和实际耗时的比值;

停顿时间: 垃圾回收的时候,由于Stop the World,应用程序的所有线程会挂起,造成应用停顿。

⚠️ 吞吐量和停顿时间是互斥的。

对于后端服务(比如后台计算任务),吞吐量优先考虑(并行垃圾回收);

对于前端应用,RT响应时间优先考虑,减少垃圾收集时的停顿时间,适用场景是Web系统(并发垃圾回收)

4.3:回收器的JVM参数

-XX:ParallelGCThreads

指定并行的垃圾回收线程的数量,最好等于CPU数量

-XX:+DisableExplicitGC

禁用System.gc(),因为它会触发Full GC,这是很浪费性能的,JVM会在需要GC的时候自己触发GC。

-XX:CMSFullGCsBeforeCompaction

在多少次GC后进行内存压缩,这个是因为并行收集器不对内存空间进行压缩的,所以运行一段时间后会产生很多碎片,使得运行效率降低。

-XX:+PrintGCDetails

开启详细GC日志模式,日志的格式是和所使用的算法有关

-XX:+PrintGCDateStamps

将时间和日期也加入到GC日志中

二:线程Dump分析

Thread Dump是非常有用的诊断Java应用问题的工具。

每一个Java虚拟机都有及时生成所有线程在某一点状态的thread-dump的能力

虽然各个 Java虚拟机打印的thread dump略有不同,但是 大多都提供了当前活动线程的快照,及JVM中所有Java线程的堆栈跟踪信息

堆栈信息一般包含完整的类名及所执行的方法,如果可能的话还有源代码的行数

Thread Dump 特点

  1. 能在各种操作系统下使用;
  2. 能在各种Java应用服务器下使用;
  3. 能在生产环境下使用而不影响系统的性能;
  4. 能将问题直接定位到应用程序的代码行上

1:thread Dump 抓取

一般当服务器挂起,崩溃或者性能低下时,就需要抓取服务器的线程堆栈(Thread Dump)用于后续的分析。

在实际运行中,往往一次 dump的信息,还不足以确认问题。为了反映线程状态的动态变化,需要接连多次做thread dump,每次间隔10-20s

建议至少产生三次 dump信息,如果每次 dump都指向同一个问题,我们才确定问题的典型性

操作系统命令获取ThreadDump

ps -ef | grep java
kill -3 <pid>

JVM 自带的工具获取线程堆栈

jps 或 ps –ef | grep java (获取PID)
jstack [-l ] <pid> | tee -a jstack.log(获取ThreadDump)

在这里插入图片描述

2:thread Dump 分析

2.1:信息说明

头部信息:时间,JVM信息

2011-11-02 19:05:06  
Full thread dump Java HotSpot(TM) Server VM (16.3-b01 mixed mode): 

线程INFO信息块

1. "Timer-0" daemon prio=10 tid=0xac190c00 nid=0xaef in Object.wait() [0xae77d000] 
# 线程名称:Timer-0;线程类型:daemon;优先级: 10,默认是5;
# JVM线程id:tid=0xac190c00,JVM内部线程的唯一标识(通过java.lang.Thread.getId()获取,通常用自增方式实现)。
# 对应系统线程id(NativeThread ID):nid=0xaef,和top命令查看的线程pid对应,不过一个是10进制,一个是16进制。(通过命令:top -H -p pid,可以查看该进程的所有线程信息)
# 线程状态:in Object.wait();
# 起始栈地址:[0xae77d000],对象的内存地址,通过JVM内存查看工具,能够看出线程是在哪儿个对象上等待;
2.  java.lang.Thread.State: TIMED_WAITING (on object monitor)
3.  at java.lang.Object.wait(Native Method)
4.  -waiting on <0xb3885f60> (a java.util.TaskQueue)     # 继续wait 
5.  at java.util.TimerThread.mainLoop(Timer.java:509)
6.  -locked <0xb3885f60> (a java.util.TaskQueue)         # 已经locked
7.  at java.util.TimerThread.run(Timer.java:462)
Java thread statck trace:是上面2-7行的信息。到目前为止这是最重要的数据,Java stack trace提供了大部分信息来精确定位问题根源。

程序先执行的是第7行,然后是第6行,依次类推。

- locked <0xb3885f60> (a java.util.ArrayList)
- waiting on <0xb3885f60> (a java.util.ArrayList) 

也就是说对象先上锁,锁住对象0xb3885f60,然后释放该对象锁,进入waiting状态

synchronized(obj) {
    ...;
    obj.wait();
    ...;
}

在堆栈的第一行信息中,进一步标明了线程在代码级的状态,例如:TIMED_WAITING (parking)

|blocked|
> This thread tried to enter asynchronized block, but the lock was taken by another thread. This thread isblocked until the lock gets released.

|blocked (on thin lock)|
> This is the same state asblocked, but the lock in question is a thin lock.

|waiting|
> This thread calledObject.wait() on an object. The thread will remain there until some otherthread sends a notification to that object.

|sleeping|
> This thread calledjava.lang.Thread.sleep().

|parked|
> This thread calledjava.util.concurrent.locks.LockSupport.park().

|suspended|
> The thread's execution wassuspended by java.lang.Thread.suspend() or a JVMTI agent call.
2.2:状态分析

线程的状态是一个很重要的东西,因此thread dump中会显示这些状态,通过对这些状态的分析,能够得出线程的运行状况,进而发现可能存在的问题

public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

NEW

每一个线程,在堆内存中都有一个对应的Thread对象。Thread t = new Thread();

当刚刚在堆内存中创建Thread对象,还没有调用t.start()方法之前,线程就处在NEW状态。

在这个状态上,线程与普通的java对象没有什么区别,就仅仅是一个堆内存中的对象

RUNNABLE

该状态表示线程具备所有运行条件,在运行队列中准备操作系统的调度,或者正在运行。

这个状态的线程比较正常,但如果线程长时间停留在在这个状态就不正常了,这说明线程运行的时间很长(存在性能问题),或者是线程一直得不得执行的机会(存在线程饥饿的问题)

BLOCKED

线程正在等待获取java对象的监视器(也叫内置锁),即线程正在等待进入由synchronized保护的方法或者代码块。

synchronized用来保证原子性,任意时刻最多只能由一个线程进入该临界区域,其他线程只能排队等待

WAITING

处在该线程的状态,正在等待某个事件的发生,只有特定的条件满足,才能获得执行机会。

而产生这个特定的事件,通常都是另一个线程。也就是说,如果不发生特定的事件,那么处在该状态的线程一直等待,不能获取执行的机会。

比如:

  • 如果A线程调用了obj对象的obj.wait()方法,如果没有线程调用obj.notifyobj.notifyAll,那么A线程就没有办法恢复运行;
  • 如果A线程调用了LockSupport.park(),没有别的线程调用LockSupport.unpark(A),那么A没有办法恢复运行。

TIMED_WAITING

J.U.C中很多与线程相关类,都提供了限时版本和不限时版本的API。

TIMED_WAITING意味着线程调用了限时版本的API,正在等待时间流逝。

当等待时间过去后,线程一样可以恢复运行。如果线程进入了WAITING状态,一定要特定的事件发生才能恢复运行;

而处在TIMED_WAITING的线程,如果特定的事件发生或者是时间流逝完毕,都会恢复运行。

TERMINATED

线程执行完毕,执行完run方法正常返回,或者抛出了运行时异常而结束,线程都会停留在这个状态。

这个时候线程只剩下Thread对象了,没有什么用了

3:案例分析

在这里插入图片描述

CPU飙高,load高,响应非常的慢

  1. 一个请求过程中多次dump;
  2. 对比多次dump文件的runnable线程,如果执行的方法有比较大变化,说明比较正常。如果在执行同一个方法,就有一些问题了;

查找占用CPU最多的线程

  1. 使用命令:top -H -p <pid>,找到导致CPU高的线程ID,对应thread dump信息中线程的nid,只不过一个是十进制,一个是十六进制;
  2. 在thread dump中,根据top命令查找的线程id,查找对应的线程堆栈信息

CPU使用率不高但是响应十分的慢

进行dump,查看是否有很多thread struck在了i/o、数据库等地方,定位瓶颈原因

请求无法响应

多次dump,对比是否所有的runnable线程都一直在执行相同的方法,如果是的,说明发生了死锁

死锁
死锁经常表现为程序的停顿,或者不再响应用户的请求。从操作系统上观察,对应进程的CPU占用率为零,很快会从top或prstat的输出中消失。

比如在下面这个示例中,是个较为典型的死锁情况

"Thread-1" prio=5 tid=0x00acc490 nid=0xe50 waiting for monitor entry [0x02d3f000 
..0x02d3fd68] 
at deadlockthreads.TestThread.run(TestThread.java:31) 
- waiting to lock <0x22c19f18> (a java.lang.Object) 
- locked <0x22c19f20> (a java.lang.Object) 

"Thread-0" prio=5 tid=0x00accdb0 nid=0xdec waiting for monitor entry [0x02cff000 
..0x02cff9e8] 
at deadlockthreads.TestThread.run(TestThread.java:31) 
- waiting to lock <0x22c19f20> (a java.lang.Object) 
- locked <0x22c19f18> (a java.lang.Object)

在 JAVA 5中加强了对死锁的检测。线程 Dump中可以直接报告出 Java级别的死锁

热锁

热锁,也往往是导致系统性能瓶颈的主要因素。

其表现特征为:由于多个线程对临界区,或者锁的竞争,可能出现:

  • 频繁的线程的上下文切换:从操作系统对线程的调度来看,当线程在等待资源而阻塞的时候,操作系统会将之切换出来,放到等待的队列,当线程获得资源之后,调度算法会将这个线程切换进去,放到执行队列中。
  • 大量的系统调用:因为线程的上下文切换,以及热锁的竞争,或者临界区的频繁的进出,都可能导致大量的系统调用。
  • 大部分CPU开销用在“系统态”:线程上下文切换,和系统调用,都会导致 CPU在 “系统态 ”运行,换而言之,虽然系统很忙碌,但是CPU用在 “用户态 ”的比例较小,应用程序得不到充分的 CPU资源。
  • 随着CPU数目的增多,系统的性能反而下降。因为CPU数目多,同时运行的线程就越多,可能就会造成更频繁的线程上下文切换和系统态的CPU开销,从而导致更糟糕的性能。

上面的描述,都是一个 scalability(可扩展性)很差的系统的表现。

从整体的性能指标看,由于线程热锁的存在,程序的响应时间会变长,吞吐量会降低。

那么,怎么去了解 “热锁 ”出现在什么地方呢?

一个重要的方法是 结合操作系统的各种工具观察系统资源使用状况,以及收集Java线程的DUMP信息,看线程都阻塞在什么方法上

了解原因,才能找到对应的解决方法。

4:JVM重要线程

JVM运行过程中产生的一些比较重要的线程罗列如下:

线程名称解释说明
Attach ListenerAttach Listener 线程是负责接收到外部的命令,而对该命令进行执行的并把结果返回给发送者。通常我们会用一些命令去要求JVM给我们一些反馈信息,如:java -version、jmap、jstack等等。 如果该线程在JVM启动的时候没有初始化,那么,则会在用户第一次执行JVM命令时,得到启动。
Signal Dispatcher前面提到Attach Listener线程的职责是接收外部JVM命令,当命令接收成功后,会交给signal dispather线程去进行分发到各个不同的模块处理命令,并且返回处理结果。signal dispather线程也是在第一次接收外部JVM命令时,进行初始化工作。
CompilerThread0用来调用JITing,实时编译装卸class 。 通常,JVM会启动多个线程来处理这部分工作,线程名称后面的数字也会累加,例如:CompilerThread1。
Concurrent Mark-Sweep GC Thread并发标记清除垃圾回收器(就是通常所说的CMS GC)线程, 该线程主要针对于老年代垃圾回收。ps:启用该垃圾回收器,需要在JVM启动参数中加上:-XX:+UseConcMarkSweepGC。
DestroyJavaVM执行main()的线程,在main执行完后调用JNI中的 jni_DestroyJavaVM() 方法唤起DestroyJavaVM 线程,处于等待状态,等待其它线程(Java线程和Native线程)退出时通知它卸载JVM。每个线程退出时,都会判断自己当前是否是整个JVM中最后一个非deamon线程,如果是,则通知DestroyJavaVM 线程卸载JVM。
Finalizer Thread这个线程也是在main线程之后创建的,其优先级为10,主要用于在垃圾收集前,调用对象的finalize()方法;关于Finalizer线程的几点:1) 只有当开始一轮垃圾收集时,才会开始调用finalize()方法;因此并不是所有对象的finalize()方法都会被执行;2) 该线程也是daemon线程,因此如果虚拟机中没有其他非daemon线程,不管该线程有没有执行完finalize()方法,JVM也会退出;3) JVM在垃圾收集时会将失去引用的对象包装成Finalizer对象(Reference的实现),并放入ReferenceQueue,由Finalizer线程来处理;最后将该Finalizer对象的引用置为null,由垃圾收集器来回收;4) JVM为什么要单独用一个线程来执行finalize()方法呢?如果JVM的垃圾收集线程自己来做,很有可能由于在finalize()方法中误操作导致GC线程停止或不可控,这对GC线程来说是一种灾难;
Low Memory Detector这个线程是负责对可使用内存进行检测,如果发现可用内存低,分配新的内存空间。
Reference HandlerJVM在创建main线程后就创建Reference Handler线程,其优先级最高,为10,它主要用于处理引用对象本身(软引用、弱引用、虚引用)的垃圾回收问题 。
VM Thread这个线程就比较牛b了,是JVM里面的线程母体,根据hotspot源码(vmThread.hpp)里面的注释,它是一个单个的对象(最原始的线程)会产生或触发所有其他的线程,这个单个的VM线程是会被其他线程所使用来做一些VM操作(如:清扫垃圾等)。

三:生产环境下的JVM & GC配置建议

1:JVM,GC配置建议

  • 大多数情况JVM调优考虑调整下面三个方面
    • 最大堆和最小堆大小
    • GC收集器
    • 新生代(年轻代)大小
  • 在没有全面监控,收集性能数据之前,调优就是扯淡
  • 99%的情况是代码出了问题,而不是JVM的参数不对
  • 不谈业务的调优都是在耍流氓

JVM优化选项

  • 1.8优先使用G1收集器,摆脱各种选项的烦恼
  • 最常见的配置的几个参数示例如下:
java -jar 
	-XX:+UseG1GC # 优先使用G1收集器,摆脱各种选项的烦恼
	-Xms2G -Xmx2G  # 这两个参数设置最好相同,减少内存的交换,增加性能
	-Xss256k # 虚拟机栈的空间一般设置成为128k就足够用了,超过256k考虑优化,不建议超过256k
	-XX:MaxGCPauseMills=300 # 最多300ms的STW时间,200~500之间,增大可以减少GC的次数,提高吞吐
	-Xloggc:/logs/gc.log
	-XX:+PrintGCTimeStamps
	-XX:+PrintGCDetails # 打印用的
test.jar
  • Xmx方法第一次起始设置的大一点,跟踪监控日志,调整为堆峰值的2 ~ 3倍大小
  • G1一般不考虑新生代的大小,因为G1新生代的大小是动态调整的

2:OOM排查

2.1:基本排查

1:查看在服务器上有哪些Java的进程在运行:

jps

2:检查指定java进程的内存和GC的使用情况

# gcutil -> 打印gc相关的信息
# 17038 -> 要查看进程的端口号,1000 -> 刷新间隔为1s, 10 -> 共执行10次
jstat -gcutil 17038 1000 10

在这里插入图片描述

2:arthas分析工具

1:下载Arthas到服务器上

curl -O https://alibaba.github.io/arthas/arthas-boot.jar

2:使用也很简单,就是运行这个jar

java -jar arthas-boot.jar # 启动arthas

在这里插入图片描述

3:arthas dashboard仪表盘

在这里插入图片描述

4:dump和图标化分析

在这里插入图片描述

5:导入dump文件:

visualVM -> file -> Load -> 找到dump下来的文件

在这里插入图片描述
在这里插入图片描述

6:在新页签中打开对应的你怀疑的实例

在这里插入图片描述

找到调用栈:
在这里插入图片描述

3:arthas的其他使用说明

先推荐两个东西:

  • 一个是 Arthas 官网:https://arthas.aliyun.com/doc/ 官方文档对 Arthas 的每个命令都做出了介绍和解释,并且还有在线教程
  • 另外还有一个向大家推荐的是一款名为 Arthas Idea 的 IDEA 插件,能快速生成 Arthas命令的插件,可快速生成可用于该类或该方法的 Arthas 命令,大大提高排查问题的效率

在这里插入图片描述

3.1:类命令

getstatic

查看类的静态属性。推荐直接使用 ognl 命令,更加灵活。

# getstatic class_name field_name
getstatic demo.MathGame random

# 如果该静态属性是一个复杂对象,还可以支持在该属性上通过 ognl 表达式进行遍历,过滤,访问对象的内部属性等操作。
# 例如,假设 n 是一个 Map,Map 的 Key 是一个 Enum,我们想过滤出 Map 中 Key 为某个 Enum 的值,可以写如下命令
getstatic com.alibaba.arthas.Test n 'entrySet().iterator.{? #this.key.name()=="STOP"}'

jad

反编译指定已加载类的源码。jad 只能反编译单个类,如需批量下载指定包的目录的 class 字节码请使用 dump 命令。

比如我们想知道自己提交的代码是否生效了,这种场景jad 命令就特别有用。

# 反编译 java.lang.String
jad java.lang.String

# 默认情况下,反编译结果里会带有 ClassLoader 信息,通过 --source-only 选项,可以只打印源代码。方便和 mc/retransform 命令结合使用。
jad --source-only java.lang.String

# 反编译指定的函数
jad java.lang.String substring

# 当有多个 ClassLoader 都加载了这个类时,jad 命令会输出对应 ClassLoader 实例的 hashcode
# 然后你只需要重新执行 jad 命令,并使用参数 -c <hashcode> 就可以反编译指定 ClassLoader 加载的那个类了
jad org.apache.log4j.Logger -c 69dcaba4

retransform

加载外部的 .class 文件,retransform jvm 已加载的类。

# 结合 jad/mc 命令使用,jad 命令反编译,然后可以用其它编译器,比如 vim 来修改源码
jad --source-only com.example.demo.arthas.user.UserController > /tmp/UserController.java

# mc 命令来内存编译修改过的代码
mc /tmp/UserController.java -d /tmp

# 用 retransform 命令加载新的字节码
retransform /tmp/com/example/demo/arthas/user/UserController.class

加载指定的 .class 文件,然后解析出 class name,再 retransform jvm 中已加载的对应的类。

每加载一个 .class 文件,则会记录一个 retransform entry。

如果多次执行 retransform 加载同一个 class 文件,则会有多条 retransform entry。

# 查看 retransform entry
retransform -l

# 删除指定 retransform entry,需要指定 id:
retransform -d 1

# 删除所有 retransform entry
retransform --deleteAll

# 显式触发 retransform
retransform --classPattern demo.MathGame

如果对某个类执行 retransform 之后,想消除 retransform 的影响,则需要:

  • 删除这个类对应的 retransform entry。
  • 重新显式触发 retransform。

retransform 的限制:

  • 不允许新增加 field/method。
  • 正在跑的函数,没有退出不能生效。

使用 mc 命令来编译 jad 的反编译的代码有可能失败。可以在本地修改代码,编译好后再上传到服务器上。

有的服务器不允许直接上传文件,可以使用 base64 命令来绕过。

  1. 在本地先转换 .class 文件为 base64,再保存为 result.txt。
base64  -i /tmp/test.class -o /tmp/result.txt
  1. 到服务器上,新建并编辑 result.txt,复制本地的内容,粘贴再保存。
vim  /tmp/result.txt
  1. 把服务器上的 result.txt 还原为.class
base64 -d /tmp/result.txt > /tmp/test.class
  1. 用 md5 命令计算哈希值,校验是否一致
md5sum  /tmp/test.class
3.2:监测排查命令【最常用】

这些命令,都通过字节码增强技术来实现的,会在指定类的方法中插入一些切面来实现数据统计和观测

因此在线上、预发使用时,请尽量明确需要观测的类、方法以及条件,诊断结束要执行 stop 或将增强过的类执行 reset 命令

monitor

方法执行监控。可对方法的调用次数,成功次数,失败次数等维度进行统计

# -b:计算条件表达式过滤统计结果(方法执行完毕之前),默认是方法执行之后过滤
# -c:统计周期,默认值为 120 秒
# params[0] <= 2:过滤条件,方法第一个参数小于等于2
monitor -b -c 5 com.test.testes.MathGame primeFactors "params[0] <= 2"

stack

输出当前方法被调用的调用路径。

很多时候我们都知道一个方法被执行,但这个方法被执行的路径非常多,或者你根本就不知道这个方法是从那里被执行了,此时你需要的是 stack 命令

# -n:执行次数
stack demo.MathGame primeFactors  -n  2

thread

查看当前线程信息,查看线程的堆栈。

# 没有参数时,默认按照 CPU 增量时间降序排列,只显示第一页数据
# -i 1000: 统计最近 1000ms 内的线程 CPU 时间
# -n 3: 展示当前最忙的前 N 个线程并打印堆栈
# --state WAITING:查看指定状态的线程
thread

# 显示指定线程的运行堆栈
thread id

# 找出当前阻塞其他线程的线程,注意,目前只支持找出 synchronized 关键字阻塞住的线程, 如果是 java.util.concurrent.Lock 目前还不支持。
thread -b

输出:

  • Internal 表示为 JVM 内部线程,参考 dashboard 命令的介绍。
  • cpuUsage 为采样间隔时间内线程的 CPU 使用率,与 dashboard 命令的数据一致。
  • deltaTime 为采样间隔时间内线程的增量 CPU 时间,小于 1ms 时被取整显示为 0ms。
  • time 为线程运行总 CPU 时间。

trace

方法内部调用路径,并输出方法路径上的每个节点上耗时。

trace 命令在定位性能问题的时候特别有用

# -n 1:限制匹配次数
# --skipJDKMethod false:默认情况下,trace 不会包含 jdk 里的函数调用,如果希望 trace jdk 里的函数,需要显式设置
# --exclude-class-pattern :排除掉指定的类
trace javax.servlet.Filter * -n 1 --skipJDKMethod false --exclude-class-pattern com.demo.TestFilter

# 正则表达式匹配路径上的多个类和函数,达到多层 trace 的效果
trace -E com.test.ClassA|org.test.ClassB method1|method2|method3

watch

函数执行数据观测,通过编写 OGNL 表达式进行对应变量的查看。

  • watch 命令定义了 4 个观察事件点,即 -b 函数调用前,-e 函数异常后,-s 函数返回后,-f 函数结束后。
  • 4 个观察事件点 -b-e-s 默认关闭,-f 默认打开,当指定观察点被打开后,在相应事件点会对观察表达式进行求值并输出。
  • 这里要注意函数入参函数出参的区别,有可能在中间被修改导致前后不一致,除了 -b 事件点 params 代表函数入参外,其余事件都代表函数出参。
  • 当使用 -b 时,由于观察事件点是在函数调用前,此时返回值或异常均不存在。
  • 在 watch 命令的结果里,会打印出location信息。location有三种可能值:AtEnterAtExitAtExceptionExit。对应函数入口,函数正常 return,函数抛出异常。
# -x表示遍历深度,可以调整来打印具体的参数和结果内容,默认值是 1。
 # -x最大值是 4,防止展开结果占用太多内存。用户可以在ognl表达式里指定更具体的 field。
 watch demo.MathGame primeFactors -x 3
 
 # 可以使用ognl表达式进行条件过滤
 watch demo.MathGame primeFactors "{params[0],target}" "params[0]<0" "#cost>200"
 
 # 可以使用 target.field_name 访问当前对象的某个属性
 watch demo.MathGame primeFactors 'target.illegalArgumentCount'
 
 #  watch 构造函数
 watch demo.MathGame <init> '{params,returnObj,throwExp}' -v
 
 # watch内部类
 watch OuterClass$InnerClass
3.3:JVM命令

heapdump

生成堆转储文件。

# dump 到指定文件
heapdump arthas-output/dump.hprof

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

jfr

Java Flight Recorder (JFR) 是一种用于收集有关正在运行的 Java 应用程序的诊断和分析数据的工具。

它集成到 Java 虚拟机 (JVM) 中,几乎不会造成性能开销,因此即使在负载较重的生产环境中也可以使用。

# 启动 JFR 记录
jfr start

# 启动 jfr 记录,指定记录名,记录持续时间,记录文件保存路径。
# --duration  JFR 记录持续时间,支持单位配置,60s, 2m, 5h, 3d,不带单位就是秒,默认一直记录。
jfr start -n myRecording --duration 60s -f /tmp/myRecording.jfr

# 查看所有 JFR 记录信息
jfr status

# 查看指定记录 id 的记录信息
jfr status -r 1

# 查看指定状态的记录信息
jfr status --state closed

# jfr dump 会输出从开始到运行该命令这段时间内的记录到 JFR 文件,且不会停止 jfr 的记录
# 生成的结果可以用支持 jfr 格式的工具来查看。比如:JDK Mission Control : https://github.com/openjdk/jmc
jfr dump -r 1 -f /tmp/myRecording1.jfr

# 停止 jfr 记录
jfr stop -r 1

memory

查看 JVM 内存信息。

Memory                           used      total      max        usage
heap                             32M       256M       4096M      0.79%
g1_eden_space                    11M       68M        -1         16.18%
g1_old_gen                       17M       184M       4096M      0.43%
g1_survivor_space                4M        4M         -1         100.00%
nonheap                          35M       39M        -1         89.55%
codeheap_'non-nmethods'          1M        2M         5M         20.53%
metaspace                        26M       27M        -1         96.88%
codeheap_'profiled_nmethods'     4M        4M         117M       3.57%
compressed_class_space           2M        3M         1024M      0.29%
codeheap_'non-profiled_nmethods' 685K      2496K      120032K    0.57%
mapped                           0K        0K         -          0.00%
direct                           48M       48M        -          100.00%

dashboard

当前系统的实时数据面板,按 ctrl+c 退出。

# i:刷新实时数据的时间间隔 (ms),默认 5000m
# n:刷新实时数据的次数
dashboard -i 5000 -n 3

显示 ID 为 -1 的是 JVM的内部线程,JVM 内部线程包括下面几种:

  • JIT 编译线程:如 C1 CompilerThread0, C2 CompilerThread0
  • GC 线程:如 GC Thread0, G1 Young RemSet Sampling。
  • 其它内部线程:如 VM Periodic Task Thread, VM Thread, Service Thread。

当 JVM 堆(heap)/元数据(metaspace) 空间不足或 OOM 时, GC 线程的 CPU 占用率会明显高于其他的线程。

classloader

classloader 命令将 JVM 中所有的 classloader 的信息统计出来,并可以展示继承树,urls 等

# 按类加载类型查看统计信息
classloader

# 按类加载实例查看统计信息
classloader -l

# 查看 ClassLoader 的继承树
classloader -t

# 查看 URLClassLoader 实际的 urls,通过 classloader -l 可以获取到哈希值
classloader -c 3d4eac69

logger

查看 logger 信息,更新 logger level

# 查看所有 logger 信息
logger

# 查看指定名字的 logger 信息
logger -n org.springframework.web

# 更新 logger level
logger --name ROOT --level debug

sc

查看 JVM 已加载的类信息。

# 模糊搜索
sc demo.*

# 打印类的详细信息
sc -d demo.MathGame

# 打印出类的 Field 信息
sc -d -f demo.MathGame

mbean

查看 Mbean 的信息。

所谓 MBean 就是托管的Java对象,类似于 JavaBeans 组件,遵循 JMX(Java Management Extensions,即Java管理扩展) 规范中规定的设计模式。

MBean可以表示任何需要管理的资源。

# 列出所有 Mbean 的名称
mbean

# 查看 Mbean 的元信息
mbean -m java.lang:type=Threading

# 查看 mbean 属性信息,mbean 的 name 支持通配符匹配 mbean java.lang:type=Th*
mbean java.lang:type=Threading

#通配符匹配特定的属性字段
mbean java.lang:type=Threading *Count

# 实时监控使用-i,使用-n命令执行命令的次数(默认为 100 次)
mbean -i 1000 -n 50 java.lang:type=Threading *Count

比如我们可以使用 mbean 命令来查看 Druid 连接池的属性:

mbean com.alibaba.druid.pool:name=dataSource,type=DruidDataSource

profiler

生成应用热点的火焰图。本质上是通过不断的采样,然后把收集到的采样结果生成火焰图

# 启动 profiler
# 生成的是 cpu 的火焰图,即 event 为cpu。可以用--event参数来指定。
profiler start --event cpu

# 获取已采集的 sample 的数量
profiler getSamples

# 查看 profiler 状态
profiler status

# 停止 profiler,生成结果,结果文件是html格式,也可以用--format参数指定
profiler stop --format html

# 恢复采样,start和resume的区别是:start是新开始采样,resume会保留上次stop时的数据。
profiler resume

# 配置 include/exclude 来过滤数据
profiler start --include 'java/*' --include 'demo/*' --exclude '*Unsafe.park*'

# 生成 jfr 格式结果
profiler start --file /tmp/test.jfr

vmoption

查看,更新 VM 诊断相关的参数

# 查看所有的 option
vmoption

# 查看指定的 option
vmoption PrintGC

# 更新指定的 option
vmoption PrintGC true

vmtool

vmtool 利用 JVMTI 接口,实现查询内存对象,强制 GC 等功能。

# --limit:可以限制返回值数量,避免获取超大数据时对 JVM 造成压力。默认值是 10
# --action:执行的动作
vmtool --action getInstances --className java.lang.String --limit 10

#强制 GC
vmtool --action forceGc

# interrupt 指定线程
vmtool --action interruptThread -t 1
3.4:特殊命令

可以使用 -v 查看观察匹配表达式的执行结果

ognl

执行 ognl 表达式,是Arthas中最为灵活的命令。

# -c:执行表达式的 ClassLoader 的 hashcode,默认值是 SystemClassLoader
# --classLoaderClass:指定执行表达式的 ClassLoader 的 class name
# -x:结果对象的展开层次,默认值 1
ognl --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader @org.springframework.boot.SpringApplication@logger

options

全局开关,慎用!

# 查看所有的 options
options

# 设置指定的 option,默认情况下json-format为 false,如果希望watch/tt等命令结果以 json 格式输出,则可以设置json-format为 true。
options json-format true

# 默认情况下,watch/trace/tt/trace/monitor等命令不支持java.* package 下的类。可以设置unsafe为 true,则可以增强。
options unsafe true

# Arthas 默认启用strict模式,在ognl表达式里,禁止更新对象的 Property 或者调用setter函数
# 用户如果确定要在ognl表达式里更新对象,可以执行options strict false,关闭strict模式。
options strict false
3.5:帮助命令

help

查看命令帮助信息,可以查看当前 arthas 版本支持的指令,或者查看具体指令的使用说明

help dashboard 

# 或者
dashboard  -help

history

打印命令历史。

#查看最近执行的3条指令
history 3

#清空指令
history -c

cls

清空当前屏幕区域。

quit

仅退出当前的连接,Attach 到目标进程上的 arthas 还会继续运行,端口会保持开放,下次连接时可以直接连接上。

或者直接按 Q 也能退出

stop

完全退出 arthas,stop 时会重置所有增强过的类

reset

重置增强类,将被 Arthas 增强过的类全部还原,Arthas 服务端 stop 时会重置所有增强过的类

# 还原指定类
reset Test

# 还原所有类
reset

http://www.kler.cn/a/455814.html

相关文章:

  • SelectionArea 实现富文本
  • PaddleOCR文字识别模型的FineTune
  • Ftrans数据摆渡系统 搭建安全便捷跨网文件传输通道
  • SpringCloudAlibaba升级手册-nacos问题记录
  • 多个微服务 Mybatis 过程中出现了Invalid bound statement (not found)的特殊问题
  • Leetcode打卡:查询数组中元素出现的位置
  • BFS中的多源BFS、最小步数模型和双端队列广搜
  • HarmonyOS NEXT 实战之元服务:静态案例效果---教育培训服务
  • MacOS下TestHubo安装配置指南
  • Ubuntu24.04最新版本安装详细教程
  • “拍卖信息化”:网上拍卖系统的未来发展
  • 基于Spring Boot的手机卡销售系统的设计与实现
  • Redis 使用进阶:全面深入的高级用法及实际应用场景
  • 微信小程序 app.json 配置文件解析与应用
  • 在已有vue cli项目中添加单元测试配置
  • matlab遇到的各种问题及解决方案
  • macos安装maven以及.bash_profile文件优化
  • XML与Go结构互转实现(序列化及反序列化)
  • 常用 Docker 命令介绍
  • tensorflow_probability与tensorflow版本依赖关系
  • leetcode83:删除链表中的重复元素
  • LLM常见面试题(26-30题)--langchain篇
  • Android 图片优化
  • Wend看源码-Java-集合学习(List)
  • 处理元素卡在视野边界,滚动到视野内
  • 混合式框架 Tauri