监控调优

63

调优步骤

性能监控

一种非侵入方式收集或者查看应用运营性能数据的活动.监控通常是一种在生产、质量评估或者开发环境下实施的带有预防或者主动性的活动.当应用的出现性能问题却没有足够多的线索时,首先需要进行性能监控

如GC频繁、cpu负载过高、OOM、内存泄漏、死锁、应用响应时间过长

性能分析

一种侵入方式收集的运行性能数据,它可能会影响到应用的吞吐量或响应性.性能分析是针对性能问题的答复结果,关注点比性能监控更集中.性能分析很少在生产环境下进行,通常是在质量评估、系统测试或者开发环境下进行,是性能监控之后的步骤

性能调优

一种改善应用响应性或吞吐量而更改参数]源代码、属性配置,性能调优是在性能监控和性能分析之后的步骤

监控与诊断工具

命令行

对应源码 对应文档

以下命令实例中中,[]中包含的参数代表代表可选参数,<>中包含的参数代表必选参数

JPS

Java Process Status 显示当前操作系统中所有的HotSpot虚拟机的进程信息

  • -q 仅显示JVM的pid

  • -l 输出应用程序的全类名,如果是jar包则输出完整路径

  • -m 输出jvm启动时传递给main方法的参数

  • -v 列出jvm启动时的jvm参数

只显示jvm相关进程,和ps命令类似

如果关闭了默认开启的性能监控(-XX:-UsePerfData),则jps无法检测到对应的jvm

JSTAT

JVM Statistics Monitoring Tool 用于监视虚拟机的各种运行状态信息的命令行工具,可以显示本地或远程虚拟机的类装载、内存、垃圾收集、JIT编译等数据

jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]

  • option 查询参数

    • 类装载相关

      • -class 显示ClassLoader相关信息

        类的装载、类的装载总量、类的卸载、类的卸载总量、类装载器消耗时间

    • JIT相关

      • -compiler 显示JIT编译器编译过的方法、耗时等信息

      • -printcompilation 输出已经被JIT编译的方法

    • 垃圾收集相关

      • -gc 显示gc相关的堆信息

      • -gccapacity 和-gc类似,关注堆的使用到的最大最小空间

      • -gcutil 和-gc类似,关注占用堆空间百分比

      • -gccause 和-gcutil类似,关注垃圾回收事件的原因

      • -gcnew 新生代GC状况

      • -gcnewcapacity 新生代内存统计

      • -gcold 老年代gc状况

      • -gcoldcapacity 老年代内存统计

      • -gcmetacapacity 方法区内存统计

# 查看指定端口号jvm的类装载信息
jstat -class 37763
$ Loaded  Bytes  Unloaded  Bytes     Time   
   756  1791.6        0     0.0       0.03
  • -t 进程运行的时间(秒)

Timestamp  GCT   # GC时间占比0.176%
2990.6     76.601
3008.7     76.633

可以通过两次的进程运行的时间和GC时间的增量来计算GC时间占运行时间的比例

如果该比例超过20%则说明堆压力较大,如果比例超过90%则说明堆几乎没有可用空间随时会导致OOM

Timestamp      OU
2989.6      240535.2 
2995.6      240951.2
2998.6      241095.2
3001.6      241247.2
3004.6      241711.3
3007.7      243269.0

运行jstat命令连续获取多行性能数据,并取这几行数据中OU(即已占用的老年代内存)的最小值

每隔一段较长的时间重复一次上述操作,来获得多组OU最小值.如果这些值呈上涨趋势,则说明JVM老年代内存已使用量在不断上涨,也就是无法回收的对象在不断增加,因此很有可能存在内存泄漏

  • -h 每隔指定次数输出一次表头

  • vmid jvm的pid

  • interval 查询间隔(毫秒)

  • count 查询次数

# 查看指定端口号jvm的类装载信息
# 并显示运行时间戳,设置每秒打印一次,一共打印50次,每打印3次打印一次表头
jstat -class -t -h 3 37763 1000 50

JINFO

Configuration Info for Java 查看虚拟机参数配置信息,也可以用于调整虚拟机参数

可以通过jinfo命令查看某个虚拟机参数的默认值

jinfo <option> <pid>

  • -sysprops 查看指定jvm的完整虚拟机参数

和System.getProperties()取得的参数一致

jinfo -sysprops 43560 
  • -flags查看所有JVM曾赋值过的参数

jinfo -flags 46681
  • -flag 查看指定参数

jinfo -flag UseParallelGC 46681
$ -XX:+UseParallelGC

不旦可以查看还能修改,并使之立刻生效.但不是所有参数都支持动态修改,只有被标记为manageable的参数可以被实时修改

可以通过改命令查看可以被修改的参数

java -XX:+PrintFlagsFinal -version | grep manageable

开启指定JVM参数

  • 布尔类型

jinfo -flag PrintGCDetails 47624 
$ -XX:-PrintGCDetails # 未开启
jinfo -flag +PrintGCDetails 47624
jinfo -flag PrintGCDetails 47624 
$ -XX:+PrintGCDetails # 已开启
  • 数值类型

jinfo -flag MaxHeapFreeRatio=90 47624

JMAP

JVM Memory Map 获取JVM的dump文件,获取JVM进程的内存相关信息

由于jmap会访问堆中的所有对象,为了保证在此过程中不被应用现场干扰,jmap需要借助安全点的机制,让所有线程听伦在不改变堆中数据的状态.因此jmap导出的快照都是在安全点的位置,这也导致基于该快照的分析可能存在偏差,例如某个对象在两个安全点之间,那么可能无法探知到这些对象

jinfo <option> <pid>

  • -dump 生成堆转储快照

  • -dump:live 只保存堆中存活对象

实际使用一般更推荐使用-dump:live,因为它容量更小,更重要的是分析堆内存中的存活对象才是更重要的(只有存活对象才会不被回收)

jmap -dump:format=b,file=./Desktop/GCTest.hprof 60408
​
jmap -dump:live,format=b,file=./Desktop/GCTest4.hprof 60148

除了使用上述方式在生成环境中使用jmap方式生存dump文件,也可以使用在系统OOM时自动生成dump文件

使用场景不一样,自动生成可以第一时间保存导致OOM的快照信息

而使用jmap可以在系统监控异常但是还没有OOM的时候生成快照信息

-XX:+HeapDumpOnOutOfMemoryErro
-XX:+HeapDumpPath=./dump.hprof
  • -heap 生成目前整个堆空间的详细信息

部分命令jmap已经不直接支持,需要使用jhsdb

jhsdb jmap --heap --pid 3255
  • -histo 生成目前整个堆空间对象的统计信息

jmap -histo 13627
  • -F 当-dump没有响应时,添加此参数强制生成

  • -J 传递参数给jmap启动的jvm

JHAT

JVM Heap Anaysis Tool,jdk提供的dump文件分析工具,不过在jdk9之后被删除,官方建议直接使用VisualVM

通过以下命令可以分析指定dump文件,分析完毕以后可以通过127.0.0.1:7000访问简易界面查看

jhat dump.hprof
  • -J 传递参数给jmap启动的jvm

    指定jhat的堆内存大小

jhat -J-mx20g  dump.hprof
  • -port 指定http端口,默认7000

JSTACK

JVM Stack Trace 生成虚拟机当前时刻的线程快照.线程快照就是当前虚拟机内每一条线程正在 执行的方法堆栈快照

线程快照可以定位线程出现长时间停顿的原因

jstack [-l] [-e] <pid>

  • -l 显示锁的附加信息

Locked ownable synchronizers:
    - None

执行jatack打印所有线程状态

如果有死锁会在Found one Java-level deadlock中展示

jstack 41931
  • 死锁 Deadlock*

  • 等待资源 Waiting on condition*

  • 等待获取监视器 Waiting on monitor entry*

  • 阻塞 Blocked*

  • 执行中 Runnable

  • 暂停 Suspended

  • 对象等待中 TIMED_WAITING

  • 停止 Parked

jstack类似以下代码

Map<Thread, StackTraceElement[]> all = Thread.getAllStackTraces();
Set<Map.Entry<Thread, StackTraceElement[]>> entries = all.entrySet();
for (Map.Entry<Thread, StackTraceElement[]> en : entries) {
    Thread t = en.getKey();
    StackTraceElement[] value = en.getValue();
    System.out.println("[Thread name" + t.getName() + "]");
    for (StackTraceElement element : value) {
        System.out.println("\t" + element.toString());
    }
}

JCMD

命令行工具jcmd是一个多功能的工具可以用来实现前面除了jstat之外大部分命令的功能

针对指定进程查看所有支持的命令

jcmd 44930 help

以下指令类似jps -m,查看所有jvm相关进程详细信息

jcmd -l

以下命令类似jstack

jcmd 44930 Thread.print

以下命令类似jmap -histo

jcmd 44930 GC.class_histogram

以下命令类似jmap -dump

jcmd 44930 GC.heap_dump /./Desktop/cmd_dump.hpro

以下命令,让JVM执行GC

jcmd 44930 GC.run

以下命令类似jstat -gc,查看gc执行时间

jcmd 44930 VM.uptime 

以下命令类似jinfo -sysprops,查看虚拟机参数

jcmd 44930 VM.system_properties

以下命令类似jinfo -flags

jcmd 45273 VM.flags

JSTATD

以上命令只能监控本机jvm程序,为了启用远程监控,则需要配合使用statd

图形化界面

使用命令行工具虽然能获取jvm相关信息,但是他们存在一些局限

  • 无法获取方法级别的分析数据,如方法间的调用关系、各方法的调用次数和调用时间等

  • 需要登录到宿主机上

  • 通过终端输出结果展示不够直观

jconsole

在JDK中自带的java监控和管理控制台.用于对JVM中内存、线程和类等的监控,是一个基于JMX的GUI性能监控工具

打开jconsole

jconsole

Visual VM

Visual VM是一个功能强大的多合一故障诊断和性能监控可视化工具,集成了多个jdk命令行工具,可以显示虚拟机进程,及进程的配置和环境信息,监视应用程序的cpu、gc、堆、方法去及线程的信息

visual vm支持插件,可以在此浏览链接,可以直接通过客户端的Tools-Plugins,在Available Plugins中选择自己需要的插件

  • 生成dump文件

可以通过右键Heap Dump生成,或者点击Monitor右侧Heap Dump按钮生成

  • 保存dump文件

  • 读取dump文件

可以通过Select Heap Dump to Compare多个dump文件或者本地服务进行对比

  • 监控线程

  • 抽样器

查看线程的占用情况

MAT

MAT是一款功能强大的dump文件分析器,生成内存泄漏报表,方便定位并分析问题.可以用于查找内存泄漏以及查看内存消耗情况,目前支持主流的hprof和phd格式的文件

  • 导入或着实时监控

通过Acquire Heap Dump获得指定jvm的dump文件

Open Heap Dump直接分析保存的dump文件

mat_dump

  • 生成报告

每次打开dump文件都会弹出一下向导,也可以在总览下面找到

分别是 内存泄漏疑点报告,组件报告,重新打开已经运行的报告

mat_wizard

  • 泄漏疑点报告 Leak Suspects

问题1 该线程所存储的局部变量占据了71.9%,内部有Object数组被cystem class loader加载占用71.9%

  • 直方图 Histogram

右键任意Class Name, Columns-Sort By排序

右键指定Class,Merge Shortest Paths to GC Roots-exclude all phantom/weak/soft etc. references ,该Class的非虚引用软引用的引用情况

点击最右侧按钮Select Baseline,实现多个dump进行对比

点击顶部<regex>正则表达式匹配

其中Shallow Heap 浅堆(该对象消耗的内存)

浅堆指对象本身占用的内存,不包括内部引用对象的大小

Retained Heap 深堆(该对象保留集中所有对象的浅堆之和)

对象的保留集指的是该对象被回收后,可以被回收的所有对象集合(包括该对象本身),即对象的保留集可以被认为是只能通过该对象直接或间接访问的所有对象的集合,也就是被该对象持有的所有对象集合

以下对象显示了一个简单的引用关系,对象A、D的浅堆大小仅包含本身

A的深堆大小是A、C,D的深堆大小为D、E

因为B被两个对象引用,所以无法计算在深堆内

mat_shallow_Retained Heap

  • 线程概述 thread overview

点击第四个按钮查看线程概述

其中包含<local>的代表是局部变量,有内存泄漏的可能,<class>代表该Class所引用到的Class

可以右键任意Object/Stack Fame,List objects查看对象输入(被关联)和输出(关联)的引用对象(引用链),查看是否被其他对象应用导致内存泄漏

elementData代表所关联到的实例(ArrayList内实际存储数据的数组)

  • 支配树 dominator_tree

支配树体现了对象实例之间的支配关系,在对象引用图中,所有对象都指向对象B路径都经过了对象A,则认为对象A支配了对象B.若对象A是离对象B最近的一个支配对象,则认为对象A是对象B的直接支配者

Arthas

以上的一些工具都需要在服务端进行相关的配置,然后才能通过远程连接到项目进程获取相关数据.然而线上的环境大多数情况下都是隔离的

Arthas可以在线排除问题无需重启、动态追踪代码,实时监控jvm状态

  • 使用,也可以直接在后面添加jvm的pid,当显示arthas的logo时说明开启成功

java -jar arthas-boot.jar
[INFO] JAVA_HOME: /Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home
[INFO] arthas-boot version: 3.7.2
[INFO] Process 90216 already using port 3658
[INFO] Process 90216 already using port 8563

成功后也可以选择web界面输入命令

127.0.0.1:3658

如果arthas没有正确退出,重新打开需要连接上一次监听的jvm,然后执行stop指令之后,再监听新的jvm进程

否则会出现异常

  • 查看日志

cat ~/logs/arthas/arthas.log
tail -f  ~/logs/arthas/arthas.log
  • 退出 exit退出当前客户端,stop退出arthas服务端并退出所有客户端

  • 查看帮助文档

java -jar arthas-boot.jar -h

arthas中的基础命令和linux中的类似具体参考文档

或者在命令后面追加-h

  • dashboard 查看jvm的概括信息

# 每0.5秒打印一次,累计4次
dashboard -i 500 -n 4
  • thread 获取jvm的所有线程

# 指定线程id
thread 1
# block状态的线程
thread -b
# 每0.5秒打印一次,累计4次
thread -i 500 -n 4
  • sysprops jvm的属性信息

类似jinfo

  • sysenv jvm的环境变量

  • headdump 导出当前jvm的堆转储文件

heapdump ./dump.hprof
# 指定存活的对象
heapdump --live ./dump.hprof
  • sc 查看已加载的类信息

# 指定包下的类列表
sc java.*
# 该类的详细信息
sc -d java.lang.String
# 并且追加成员变量信息
sc -f -d java.lang.String
  • sm 查看方法

# 查看类所有方法
sm java.lang.String
# 查看指定方法
sm java.lang.String toString
# 查看方法详细信息
sm -d java.lang.String
  • jad 反编译代码

# 反编译指定类
jad java.lang.String
# 反编译指定类下的指定方法
jad java.lang.String toString
  • mc 编译代码

mc ./Desktop/Test.java
  • redefine 热更新代码

将mc命令编译的代码执行redefine

  • retransform 热更新代码

本地编译好的代码通过retransform热更新

  • classloader 类加载器相关信息

# 以树形结构展示类加载器
classloader -t
# 获取类加载器加载类的实例和hash
classloader -l
# 获取指定hash的类加载器的加载情况
classloader -c 764c12b6
  • monitor 方法执行监控

返回时间戳、类名、方法名、调用次数、成功次数、失败次数、平均 RT、失败率

# 每5秒显示一次指定类 构造方法执行情况
monitor -c 5 java.util.ArrayList <init>
​
# 每5秒显示一次指定类 指定方法的执行情况
monitor -c 5 java.util.ArrayList add
  • watch 观察方法的调用情况

查看方法的调用情况,返回值、抛出异常、入参

# 查看指定类的方法的入参和返回值,结果遍历深度2
watch java.util.ArrayList add "{params, returnObj}" -x 2
  • trace 该方法调用路径,并输出该方法中每个方法的耗时情况

trace java.util.ArrayList add
  • stack 输出方法的完整调用路径

stack java.util.ArrayList add
  • tt 记录同一个方法不同参数传入后的调用时间

tt传递的参数只是引用,如果参数经过后续处理,导致-p可能无法传递准确结果

# 记录
tt -t java.util.ArrayList add
​
# 查询记录列表
tt -l
​
# 筛选
tt -s 'method.name=="query"'
​
# 查看index调用信息
tt -i 1000
​
# 传递和index一样的参数,再次调用
tt -i 1000 -p
​
# 指定重载方法
tt -t *Test queryMemberDetailedDTOList params.length==1
tt -t *Test print 'params[1] instanceof Integer'
​
# 需要释放内存,否则会导致OOM
tt --delete-all
  • profiler 火焰图

# 启动profiler
profiler start
​
# 采集到的样本数量
profiler getSamples
​
# profiler状态
profiler status
​
# 停止采集并导出
# 之后可以通过http://localhost:3658/arthas-output/
profiler stop --format html

OQL

语法和sql类似

  • select查询可以指定字段,添加objects关键字可以将字符串解析为对象

  • from指定查询类

SELECT objects v.elementData FROM java.util.ArrayList v 

查询ArrayList内所有对象的保留集

select as retained set * from java.util.ArrayList

查询指定地址的结构

如果是MAT可以通过Inspetor查看对象地址

用地址的好处是可以区分不同的类加载器加载的同一种类型

SELECT * FROM  0x6c00a9058
  • where添加过滤条件

select * from char[] s where s.@length > 10
select * from java.lang.String s where s.value != null
SELECT toString(f.path.value) FROM java.io.File f