JVM - JVM调优

JVM - JVM调优

文章目录

一: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

sh 复制代码
ps -ef | grep java
kill -3 <pid>

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

sh 复制代码
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状态

java 复制代码
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中会显示这些状态,通过对这些状态的分析,能够得出线程的运行状况,进而发现可能存在的问题

java 复制代码
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的输出中消失。

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

java 复制代码
"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 Listener Attach 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 Handler JVM在创建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收集器,摆脱各种选项的烦恼
  • 最常见的配置的几个参数示例如下:
bash 复制代码
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的进程在运行:

sh 复制代码
jps

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

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

2:arthas分析工具

1:下载Arthas到服务器上

bash 复制代码
curl -O https://alibaba.github.io/arthas/arthas-boot.jar

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

sh 复制代码
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 命令,更加灵活。

sh 复制代码
# 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 命令就特别有用。

bash 复制代码
# 反编译 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 已加载的类。

bash 复制代码
# 结合 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。

bash 复制代码
# 查看 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。
sh 复制代码
base64  -i /tmp/test.class -o /tmp/result.txt
  1. 到服务器上,新建并编辑 result.txt,复制本地的内容,粘贴再保存。
sh 复制代码
vim  /tmp/result.txt
  1. 把服务器上的 result.txt 还原为.class
sh 复制代码
base64 -d /tmp/result.txt > /tmp/test.class
  1. 用 md5 命令计算哈希值,校验是否一致
sh 复制代码
md5sum  /tmp/test.class
3.2:监测排查命令【最常用】

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

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

monitor

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

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

stack

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

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

sh 复制代码
# -n:执行次数
stack demo.MathGame primeFactors  -n  2

thread

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

sh 复制代码
# 没有参数时,默认按照 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 命令在定位性能问题的时候特别有用

sh 复制代码
# -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,函数抛出异常。
sh 复制代码
# -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

生成堆转储文件。

sh 复制代码
# dump 到指定文件
heapdump arthas-output/dump.hprof

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

jfr

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

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

sh 复制代码
# 启动 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 退出。

sh 复制代码
# 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 等

sh 复制代码
# 按类加载类型查看统计信息
classloader

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

# 查看 ClassLoader 的继承树
classloader -t

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

logger

查看 logger 信息,更新 logger level

sh 复制代码
# 查看所有 logger 信息
logger

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

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

sc

查看 JVM 已加载的类信息。

sh 复制代码
# 模糊搜索
sc demo.*

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

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

mbean

查看 Mbean 的信息。

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

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

sh 复制代码
# 列出所有 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 连接池的属性:

text 复制代码
mbean com.alibaba.druid.pool:name=dataSource,type=DruidDataSource

profiler

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

bash 复制代码
# 启动 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 诊断相关的参数

sh 复制代码
# 查看所有的 option
vmoption

# 查看指定的 option
vmoption PrintGC

# 更新指定的 option
vmoption PrintGC true

vmtool

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

sh 复制代码
# --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中最为灵活的命令。

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

options

全局开关,慎用!

sh 复制代码
# 查看所有的 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 版本支持的指令,或者查看具体指令的使用说明

sh 复制代码
help dashboard 

# 或者
dashboard  -help

history

打印命令历史。

sh 复制代码
#查看最近执行的3条指令
history 3

#清空指令
history -c

cls

清空当前屏幕区域。

quit

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

或者直接按 Q 也能退出

stop

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

reset

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

sh 复制代码
# 还原指定类
reset Test

# 还原所有类
reset
相关推荐
玛丽莲茼蒿5 分钟前
javaSE 集合框架(五)——java 8新品Stream类
java·开发语言
程序员小假12 分钟前
设计一个支持万人同时抢购商品的秒杀系统?
java·后端
L***d67019 分钟前
Spring Boot(七):Swagger 接口文档
java·spring boot·后端
C雨后彩虹29 分钟前
竖直四子棋
java·数据结构·算法·华为·面试
疾风sxp33 分钟前
nl2sql技术实现自动sql生成之langchain4j SqlDatabaseContentRetriever
java·人工智能·langchain4j
一勺菠萝丶1 小时前
PDF24 转图片出现“中间横线”的根本原因与终极解决方案(DPI 原理详解)
java
姓蔡小朋友1 小时前
Unsafe类
java
一只专注api接口开发的技术猿1 小时前
如何处理淘宝 API 的请求限流与数据缓存策略
java·大数据·开发语言·数据库·spring
荒诞硬汉1 小时前
对象数组.
java·数据结构
期待のcode1 小时前
Java虚拟机的非堆内存
java·开发语言·jvm