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 特点
- 能在各种操作系统下使用;
- 能在各种Java应用服务器下使用;
- 能在生产环境下使用而不影响系统的性能;
- 能将问题直接定位到应用程序的代码行上
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.notify
或obj.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高,响应非常的慢
- 一个请求过程中多次dump;
- 对比多次dump文件的runnable线程,如果执行的方法有比较大变化,说明比较正常。如果在执行同一个方法,就有一些问题了;
查找占用CPU最多的线程
- 使用命令:
top -H -p <pid>
,找到导致CPU高的线程ID,对应thread dump信息中线程的nid,只不过一个是十进制,一个是十六进制; - 在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 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收集器,摆脱各种选项的烦恼
- 最常见的配置的几个参数示例如下:
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
命令来绕过。
- 在本地先转换
.class
文件为 base64,再保存为 result.txt。
base64 -i /tmp/test.class -o /tmp/result.txt
- 到服务器上,新建并编辑 result.txt,复制本地的内容,粘贴再保存。
vim /tmp/result.txt
- 把服务器上的
result.txt
还原为.class
base64 -d /tmp/result.txt > /tmp/test.class
- 用 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
有三种可能值:AtEnter
,AtExit
,AtExceptionExit
。对应函数入口,函数正常 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