Java性能监控与调优
[在线gc日志分析] (https://www.gceasy.io)
前言
线上问题排查相比于coding,是一个低频的工作,很多人不会经常遇到。一旦需要进行问题排查的时候,往往是重要且紧急的,因此问题排查的效率,就显得尤为重要。有些线上问题,比较直观,比如磁盘使用率高、网络流量高这种,借助合适的工具很快能定位到原因;但对于一些复杂的问题,如系统Load高、RSS占用高、内存溢出等,需要结合多方面的数据才能定位到原因。这时候,需要有正确的解题思路,并辅以合适的工具,才能高效地解决问题。
目前业界排查问题的优秀工具还是挺多的,比如国内阿里开源的Arthas、Java官方的JMC(JDK Mission Control)、Eclipse的MAT(Memory Analyzer Tooling)
问题排查的套路化
常用命令
top ps free pidstat vmstat cachestat cachetop iostat pmap
dmesg | grep -i "Out of memory"
cat /proc/meminfo | grep -i active | sort
/proc/pid/status
/proc/meminfo
strace -f -p pid
lsod -p pid
pstree -t -a -p 27458 # -t 表示显示线程,-a 表示显示命令行参数
show full processlist # 数据库命令
看进程
jps -m 非常方便直接定位所有的Java进程pid
jstack -l pid > jstack1031.log 命令打印栈信息,根据转换的16进制去jstack.log日志中查找基本能定位到具体哪行代码的问题。
看进程的线程
查找占用CPU资源最高的进程id,使用top -H -p pid 查看进程pid的所有的线程,默认是按照%CPU高~低排序。
或者直接使用top查看, shift+H显示所有的线程,默认按照%CPU高~低排序。
将pid转换为16进制,使用 printf "%x \n" <tid> 命令将线程id转成十六进制的值,然后执行 jstack -l <pid> | grep <thread-hex-id> -A 10 命令显示出错的堆栈信息,-A 10 参数用来指定显示行数,否则只会显示一行信息。
在备机切换到线上机器前,通过jmap打印出JVM堆内存信息, jmap -dump:live,format=b,file=heap1031.bin <pid>
使用jstat 命令查看GC的情况,jstat -gcutil <pid> 250 7 参数中pid是进程id,后面的250和7表示每250毫秒打印一次,总共打印7次。
jps -m
top -H -p pid
printf "%x \n" <tid>
jstack -l <pid> | grep <thread-hex-id> -A 10
Resident Set Size(常驻内存大小)
针对RSS高的问题,首先我们需要知道的是,Java进程消耗的内存绝不仅仅是你设置的Xmx或堆内存的用量这么简单,Java进程占用的内存主要分为2大部分:on-heap(堆内内存)和off-heap(堆外内存)。而堆外内存又包含JVM自身消耗的内存、JVM外的内存。所以,后续的排查思路我们也是按照堆内内存、JVM内存、JVM外内存3个方向来顺序展开。
堆内存用量的查看手段非常多,相信各个公司的基础架构团队都提供了可视化的监控手段,当然也可以通过原生命令 jcmd <pid> GC.heap_info 查看
root@davinci-mini:~# jcmd 24226 GC.heap_info
24226:
garbage-first heap total 524288K, used 228516K [0x00000000e0000000, 0x0000000100000000)
region size 1024K, 185 young (189440K), 8 survivors (8192K)
Metaspace used 71908K, capacity 74370K, committed 74624K, reserved 262144K
class space used 8688K, capacity 9577K, committed 9600K, reserved 196608K
root@davinci-mini:~#
如果Java进程的堆内存用量已接近或超过物理内存的75%,那么基本可以确定堆内存用量过大。这时可以调小Xmx来控制堆内存用量。如果Xmx不能减小,可以通过dump堆内存+MAT+ JMC(JDK Mission Control)来分析内存占用/分配情况,通过程序调优来减少堆内存用量。
是否存在大量ARENA区
如果堆内存不大,那么继续排查非堆内存。首先去看一下ARENA区,在高并发的应用中,往往ARENA区占用的内存会比较多。为什么先看ARENA区的内存占用呢?是因为这个步骤是不需要重启JVM进程就可以完成的。至于ARENA是什么以及作用是什么,可以读一下这篇文章:https://blog.csdn.net/maokelong95/article/details/51989081。
这里我们直接进入排查问题环节。执行如下命令:sudo -u <user> pmap -x <pid>|sort -gr -k2 |less,如果存在大量大小为65536或60000左右的内存区域,则很大可能是ARENA区域占用了太多的内存
root@davinci-mini:~# sudo -u root pmap -x 24226|sort -gr -k2 |more
00000000e0000000 533888 390384 390384 rw--- [ anon ]
0000fffee2657000 217764 0 0 ----- [ anon ]
0000000100960000 187008 0 0 ----- [ anon ]
0000fffeefb00000 136192 2916 0 r--s- modules
0000fffed4021000 65404 0 0 ----- [ anon ]
0000fffecc021000 65404 0 0 ----- [ anon ]
0000fffec0021000 65404 0 0 ----- [ anon ]
0000fffeb0021000 65404 0 0 ----- [ anon ]
0000fffea8021000 65404 0 0 ----- [ anon ]
0000fffea0021000 65404 0 0 ----- [ anon ]
0000fffe98021000 65404 0 0 ----- [ anon ]
0000fffe80021000 65404 0 0 ----- [ anon ]
0000fffe7c021000 65404 0 0 ----- [ anon ]
0000fffe74021000 65404 0 0 ----- [ anon ]
这种情况下,最简单粗暴的办法是在JVM启动参数中增加配置:export MALLOC_ARENA_MAX=1。需要注意的是,上述的数值只能是1,其他大于1的数值经实践证明是无法控制ARENA数量的。
非堆内存是否开销过大
如果前面2个步骤过后都没有发现问题,还有很多内存你不知道消耗在哪里了,那么我们开始第3步:开启Native Memory Tracking。前面说过,Java应用的执行,JVM自身也需要消耗一些内存的,通过开启Native Memory Tracking,我们就能知道JVM自身消耗了多少内存。
书归正传,通过修改JVM参数并重启Java进程开启NativeMemory Tracking:
-XX:NativeMemoryTracking=detail
进程重启后,可以通过NMT的一些子命令(summary/detail/baseline/diff)查看Native Memory的占用情况:sudo -u <user>jcmd <pid> VM.native_memory detail
jcmd 29671 VM.native_memory scale=MB
jcmd 29671 VM.native_memory detail scale=MB >temp.txt
root@davinci-mini:~# jcmd 26659 VM.native_memory detail scale=MB
26659:
[0x0000fffeff51e9f0] Events::init() [clone .part.6]+0xd0
[0x0000fffeff61ada4] vm_init_globals()+0x14
[0x0000fffeffb5bd24] Threads::create_vm(JavaVMInitArgs*, bool*)+0x1fc
[0x0000fffeff6c9a28] JNI_CreateJavaVM+0x80
(malloc=3KB type=Internal #1)
[0x0000fffeff51ebe0] Events::init() [clone .part.6]+0x2c0
[0x0000fffeff61ada4] vm_init_globals()+0x14
[0x0000fffeffb5bd24] Threads::create_vm(JavaVMInitArgs*, bool*)+0x1fc
[0x0000fffeff6c9a28] JNI_CreateJavaVM+0x80
(malloc=3KB type=Internal #1)
[0x0000fffeff437e7c] compileBroker_init()+0x10c
[0x0000fffeff61ae78] init_globals()+0xc0
[0x0000fffeffb5be1c] Threads::create_vm(JavaVMInitArgs*, bool*)+0x2f4
[0x0000fffeff6c9a28] JNI_CreateJavaVM+0x80
(malloc=3KB type=Internal #1)
[0x0000fffeff51eae8] Events::init() [clone .part.6]+0x1c8
[0x0000fffeff61ada4] vm_init_globals()+0x14
[0x0000fffeffb5bd24] Threads::create_vm(JavaVMInitArgs*, bool*)+0x1fc
[0x0000fffeff6c9a28] JNI_CreateJavaVM+0x80
(malloc=3KB type=Internal #1)
通过上面,可以看到JVM各个区域所使用的内存大小,主要包含了Java Heap、Class、Thread、Code、GC、Compiler、Internal、Other、Symbol等,各部分作用如下:
1)Class:加载的类与方法信息,其实就是 metaspace,包含两部分:一是 metadata,被-XX:MaxMetaspaceSize限制最大大小,另外是 class space,被
-XX:CompressedClassSpaceSize限制最大大小;
2)Thread:线程与线程栈占用内存,每个线程栈占用大小受-Xss限制,但是总大小没有限制。在x64的JVM中,Xss默认为1024K,所以如果你的应用开启了1000个线程,那么这个Thread区占用将是1024M,所以一般我们会把Xss设置为256K即满足要求;
3)Code:JIT 即时编译后(C1C2 编译器优化)的代码占用内存,受
-XX:ReservedCodeCacheSize限制;
4)GC:垃圾回收占用内存,例如垃圾回收需要的 CardTable,标记数,区域划分记录,还有标记 GC Root 等等,都需要内存。这个不受限制,一般不会很大,但也有例外,27G的堆内存,使用G1垃圾回收器,GC区居然占用了3.8G的内存;
5)Compiler:C1 C2 编译器本身的代码和标记占用的内存,这个不受限制,一般不会很大;
6)Internal:命令行解析,JVMTI 使用的内存,这个不受限制,一般不会很大;
7)Symbol: 常量池占用的大小,字符串常量池受-XX:StringTableSize个数限制,总内存大小不受限制;
我们需要需要注意的是Class、Thread、GC几个区域的大小。图4是各种JVM垃圾回收器消耗内存的比例,注意这部分内存是堆内存之外的:
截屏2021-08-26 下午7.42.07.png开启NativeMemoryTracking会造成5%的性能下降,用完记得修改JVM参数并重启永久关闭,或者可以通过以下命令临时关闭:jcmd vm.native_memory stop <pid>
堆外内存是否用量太多
堆外内存也是比较容易被忽略的一个区域,尤其是网络通信非常频繁的应用,这种应用往往大量使用Java NIO,而NIO为了提高效率,往往会申请很多的堆外内存。确认这个区域用量是否过大,最直接的方法是先查看是否是DirectByteBuffer或者MappedByteBuffer使用了较多的堆外内存。
如果你的服务器已经开启了远程JMX,你可以通过ops提供的jmx查询工具去查询,也可以通过jdk自带的工具(比如jconsole、jvisualvm)查询
如果确认上述堆外内存使用过多,那么可以通过在jvm参数中设置
-XX:MaxDirectMemorySize这个参数控制一下,因为通过DirectByteBuffer分配的堆外内存,默认是不会控制这个区域的内存用量的。
如果上述内存用量不大,那我们就需要祭出终极杀器jemalloc来做进一步分析了。
通过jemalloc的收集到的数据,我们基本能够定位到堆外内存问题的原因。
总结确认堆内存是否太大
Java进程pid:使用 jps -v / jps -l 列出java进程列表,由用户选择具体的进程;
获取运行环境的物理内存和剩余内存 free -m / free -h
Java进程的堆内存用量 jcmd GC.heap_info
Java进程GC情况 jstat -gcutil
获取到上述信息后,判断堆内存是否太大。
是否存在大量ARENA区
通过pmap命令获取内存分配列表,辅以awk命令提取内存信息,据此判断是否存在大量ARENA区。
查看一些指标命令
ulimit -c
ulimit -c unlimited
cat /proc/{pid}/limits
cat /proc/sys/vm/overcommit_memory
生产环境怎么优化
关闭 JMX,运行加 -Dspring.jmx.enabled=false 选项
运行加 -XX:TieredStopAtLevel=1 选项
运行加 -noverify 选项,无验证
惰性初始化,配置 spring.main.lazy-initialization 为 true
为什么设置-Xmx4g但是java进程内存占用达到8g?
https://laowan.blog.csdn.net/article/details/113340344
java -jar -XX:NativeMemoryTracking=detail application.jar
jcmd <pid> VM.native_memory scale=MB
jcmd <pid> VM.native_memory detail scale=MB >temp.txt
Linux中buff/cache内存占用过高解决办法
我们经常用 free -m 命令来查看系统内存的使用状态,发现buffers/cache占用的较多。
# 人工触发缓存清除
echo 1 > /proc/sys/vm/drop_caches
当然,这个文件可以设置的值分别为1、2、3。
https://blog.csdn.net/wuzhenwei0419/article/details/109744252
使用 jvisualvm 远程监控 JVM
jmx 方式
JAVA_OPTS="-Djava.rmi.server.hostname=192.168.8.229 -Dcom.sun.management.jmxremote.port=1100 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false"
# 开启 JMX 远程服务权限
# -Dcom.sun.management.jmxremote.port:配置远程 connection 的端口号
# -Dcom.sun.management.jmxremote.ssl:指定 JMX 是否启用 ssl
# -Dcom.sun.management.jmxremote.authenticate:指定 JMX 是否启用密码
# -Djava.rmi.server.hostname:配置 Server IP(不要使用 127.0.0.1)
# -Dcom.sun.management.jmxremote.rmi.port=2222
# -Dcom.sun.management.jmxremote.local.only=false
# -Dcom.sun.management.jmxremote=true
打开JDK 目录下的 bin/jvisualvm 程序,添加 JMX 连接,填写地址和端口即可
top命令VIRT,RES,SHR,DATA的含义
VIRT:virtual memory usage 虚拟内存
1、进程“需要的”虚拟内存大小,包括进程使用的库、代码、数据等
2、假如进程申请100m的内存,但实际只使用了10m,那么它会增长100m,而不是实际的使用量
RES:resident memory usage 常驻内存
1、进程当前使用的内存大小,但不包括swap out
2、包含其他进程的共享
3、如果申请100m的内存,实际使用10m,它只增长10m,与VIRT相反
4、关于库占用内存的情况,它只统计加载的库文件所占内存大小
SHR:shared memory 共享内存
1、除了自身进程的共享内存,也包括其他进程的共享内存
2、虽然进程只使用了几个共享库的函数,但它包含了整个共享库的大小
3、计算某个进程所占的物理内存大小公式:RES – SHR
4、swap out后,它将会降下来
DATA
1、数据占用的内存。如果top没有显示,按f键可以显示出来。
2、真正的该程序要求的数据空间,是真正在运行中要使用的。
top 运行中可以通过 top 的内部命令对进程的显示方式进行控制。内部命令如下:
s – 改变画面更新频率
l – 关闭或开启第一部分第一行 top 信息的表示
t – 关闭或开启第一部分第二行 Tasks 和第三行 Cpus 信息的表示
m – 关闭或开启第一部分第四行 Mem 和 第五行 Swap 信息的表示
N – 以 PID 的大小的顺序排列表示进程列表
P – 以 CPU 占用率大小的顺序排列进程列表
M – 以内存占用率大小的顺序排列进程列表
h – 显示帮助
n – 设置在进程列表所显示进程的数量
q – 退出 top
s – 改变画面更新周期
序号 列名 含义
a PID 进程id
b PPID 父进程id
c RUSER Real user name
d UID 进程所有者的用户id
e USER 进程所有者的用户名
f GROUP 进程所有者的组名
g TTY 启动进程的终端名。不是从终端启动的进程则显示为 ?
h PR 优先级
i NI nice值。负值表示高优先级,正值表示低优先级
j P 最后使用的CPU,仅在多CPU环境下有意义
k %CPU 上次更新到现在的CPU时间占用百分比
l TIME 进程使用的CPU时间总计,单位秒
m TIME+ 进程使用的CPU时间总计,单位1/100秒
n %MEM 进程使用的物理内存百分比
o VIRT 进程使用的虚拟内存总量,单位kb。VIRT=SWAP+RES
p SWAP 进程使用的虚拟内存中,被换出的大小,单位kb。
q RES 进程使用的、未被换出的物理内存大小,单位kb。RES=CODE+DATA
r CODE 可执行代码占用的物理内存大小,单位kb
s DATA 可执行代码以外的部分(数据段+栈)占用的物理内存大小,单位kb
t SHR 共享内存大小,单位kb
u nFLT 页面错误次数
v nDRT 最后一次写入到现在,被修改过的页面数。
w S 进程状态。(D=不可中断的睡眠状态,R=运行,S=睡眠,T=跟踪/停止,Z=僵尸进程)
x COMMAND 命令名/命令行
y WCHAN 若该进程在睡眠,则显示睡眠中的系统函数名
z Flags 任务标志,参考 sched.h
默认情况下仅显示比较重要的 PID、USER、PR、NI、VIRT、RES、SHR、S、%CPU、%MEM、TIME+、COMMAND 列。可以通过下面的快捷键来更改显示内容。
通过 f 键可以选择显示的内容。按 f 键之后会显示列的列表,按 a-z 即可显示或隐藏对应的列,最后按回车键确定。
按 o 键可以改变列的显示顺序。按小写的 a-z 可以将相应的列向右移动,而大写的 A-Z 可以将相应的列向左移动。最后按回车键确定。
按大写的 F 或 O 键,然后按 a-z 可以将进程按照相应的列进行排序。而大写的 R 键可以将当前的排序倒转。
JVM的参数类型
标准化参数:(各版本中保持稳定)
java -help
-server -client
java -version -showversion
-cp -classpath
非标准化参数:
X 参数(非标准化参数)
-Xint:解释执行
-Xcomp:第一次使用就编译成本地代码
-Xmixed:混合模式,JVM 自己决定是否编译成本地代码
示例:
java -version(默认是混合模式)
Java HotSpot(TM) 64-Bit Server VM (build 25.40-b25, mixed mode)
java -Xint -version
Java HotSpot(TM) 64-Bit Server VM (build 25.40-b25, interpreted mode)
XX 参数(非标准化参数)
主要用于 JVM调优和 debug
Boolean类型
格式:-XX:[+-]<name>表示启用或禁用 name 属性
如:-XX:+UseConcMarkSweepGC
-XX:+UseG1GC
非Boolean类型
格式:-XX:<name>=<value>表示 name 属性的值是 value
如:
-XX:MaxGCPauseMillis=500
-xx:GCTimeRatio=19
-Xmx -Xms属于 XX 参数
-Xms 等价于-XX:InitialHeapSize
-Xmx 等价于-XX:MaxHeapSize
-xss 等价于-XX:ThreadStackSize
动态查看JVM参数
jinfo -flag MaxHeapSize <pid>
-XX:+PrintFlagsInitial
-XX:+PrintFlagsFinal
-XX:+UnlockExperimentalVMOptions 解锁实验参数
-XX:+UnlockDiagnosticVMOptions 解锁诊断参数
-XX:+PrintCommandLineFlags 打印命令行参数
输出结果中=表示默认值,:=表示被用户或 JVM 修改后的值
示例:java -XX:+PrintFlagsFinal -version
补充:测试中需要用到 Tomcat,CentOS 7安装示例如下
sudo yum -y install java-1.8.0-openjdk*
wget http://mirror.bit.edu.cn/apache/tomcat/tomcat-8/v8.5.32/bin/apache-tomcat-8.5.32.tar.gz
tar -zxvf apache-tomcat-8.5.32.tar.gz
mv apache-tomcat-8.5.32 tomcat
cd tomcat/bin/
sh startup.sh
tomcat pid 可通过类似 ps -ef|grep tomcat或 jps来进行查看
jps
详情参考 jps官方文档
jstack
使用 jstack -l PID
命令来查看线程日志
jconsole
使用 jconsole 需要打开 JDK 的 bin 目录,找到 jconsole 并双击打开
jvisualvm
jvisualvm 也在 JDK 的 bin 目录中,同样是双击打开
jmc
jmc 是 Oracle Java Mission Control 的缩写,是一个对 Java 程序进行管理、监控、概要分析和故障排查的工具套件。它也是在 JDK 的 bin 目录中,同样是双击启动
jinfo,查看已经运行的JVM参数
jinfo -flag MaxHeapSize <pid>
jinfo -flag UseG1GC <pid>
jinfo -flags <pid>
jstat,可以查看类加载,垃圾回收信息
详情参考 jstat 官方文档
类加载
# 以下1000表每隔1000ms 即1秒,共输出10次
jstat -class pid 1000 10
垃圾收集,-gc, -gcutil, -gccause, -gcnew, -gcold
jstat -gc pid 1000 10
以下大小的单位均为 KB
S0C, S1C, S0U, S1U: S0和 S1的总量和使用量
EC, EU: Eden区总量与使用量
OC, OU: Old区总量与使用量
MC, MU: Metacspace区(jdk1.8前为 PermGen)总量与使用量
CCSC, CCSU: 压缩类区总量与使用量
YGC, YGCT: YoungGC 的次数与时间
FGC, FGCT: FullGC 的次数与时间
GCT: 总的 GC 时间
反编译.class文件,javac是编译.java --> .class
javap -c Main.class > Main.txt
GC调优步骤:
1、打印GC日志,Tomcat则直接加在JAVA_OPTS变量里
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:./gc.log
2、分析日志得到关键性指标
3、分析GC原因,调优JVM参数
https://blog.csdn.net/moxiaomo0804/article/details/104722083