JDK Tools

jps

用于显示当前用户下的所有java进程信息:

# jps [options] [hostid] 
# q:仅输出VM标识符, m: 输出main method的参数,l:输出完全的包名, v:输出jvm参数
[root@localhost ~]# jps -l
28729 sun.tools.jps.Jps
23789 cn.ms.test.DemoMain
23651 cn.ms.test.TestMain

jstat 

用于监视虚拟机运行时状态信息(类装载、内存、垃圾收集、JIT编译等运行数据)

-gc:垃圾回收统计(大小)

# 每隔2000ms输出<pid>进程的gc情况,一共输出2次
[root@localhost ~]# jstat -gc <pid> 2000 2
# 每隔2s输出<pid>进程的gc情况,每个3条记录就打印隐藏列标题
[root@localhost ~]# jstat -gc -t -h3 <pid> 2s
Timestamp        S0C    S1C    S0U    S1U    ... YGC     YGCT    FGC    FGCT     GCT   
         1021.6 1024.0 1024.0  0.0   1024.0  ...  1    0.012   0      0.000    0.012
         1023.7 1024.0 1024.0  0.0   1024.0  ...  1    0.012   0      0.000    0.012
         1025.7 1024.0 1024.0  0.0   1024.0  ...  1    0.012   0      0.000    0.012
Timestamp        S0C    S1C    S0U    S1U    ... YGC     YGCT    FGC    FGCT     GCT   
         1027.7 1024.0 1024.0  0.0   1024.0  ...  1    0.012   0      0.000    0.012
         1029.7 1024.0 1024.0  0.0   1024.0  ...  1    0.012   0      0.000    0.012
# 结果说明: C即Capacity 总容量,U即Used 已使用的容量
##########################
# S0C:年轻代中第一个survivor(幸存区)的容量 (kb)
# S1C:年轻代中第二个survivor(幸存区)的容量 (kb)
# S0U:年轻代中第一个survivor(幸存区)目前已使用空间 (kb)
# S1U:年轻代中第二个survivor(幸存区)目前已使用空间 (kb)
# EC:年轻代中Eden(伊甸园)的容量 (kb)
# EU:年轻代中Eden(伊甸园)目前已使用空间 (kb)
# OC:Old代的容量 (kb)
# OU:Old代目前已使用空间 (kb)
# PC:Perm(持久代)的容量 (kb)
# PU:Perm(持久代)目前已使用空间 (kb)
# YGC:从应用程序启动到采样时年轻代中gc次数
# YGCT:从应用程序启动到采样时年轻代中gc所用时间(s)
# FGC:从应用程序启动到采样时old代(全gc)gc次数
# FGCT:从应用程序启动到采样时old代(全gc)gc所用时间(s)
# GCT:从应用程序启动到采样时gc用的总时间(s)

-gcutil:垃圾回收统计(百分比)

[root@localhost bin]# jstat -gcutil <pid>
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
  0.00  99.80  16.21  26.18  93.34  90.74      9    0.056     2    0.045    0.102
# 结果说明
##########################
# S0:年轻代中第一个survivor(幸存区)已使用的占当前容量百分比
# S1:年轻代中第二个survivor(幸存区)已使用的占当前容量百分比
# E:年轻代中Eden(伊甸园)已使用的占当前容量百分比
# O:old代已使用的占当前容量百分比
# P:perm代已使用的占当前容量百分比
# YGC:从应用程序启动到采样时年轻代中gc次数
# YGCT:从应用程序启动到采样时年轻代中gc所用时间(s)
# FGC:从应用程序启动到采样时old代(全gc)gc次数
# FGCT:从应用程序启动到采样时old代(全gc)gc所用时间(s)
# GCT:从应用程序启动到采样时gc用的总时间(s)

-gccapacity:堆内存统计

[root@localhost ~]# jstat -gccapacity <pid>
 NGCMN    NGCMX     NGC     S0C   S1C       EC      OGCMN      OGCMX       OGC         OC      PGCMN    PGCMX     PGC       PC     YGC    FGC 
 84480.0 1349632.0 913408.0 54272.0 51200.0 502784.0   168448.0  2699264.0   168448.0   168448.0  21504.0  83968.0  51712.0  51712.0      9     0
# 结果说明
##########################
# NGCMN:年轻代(young)中初始化(最小)的大小 (kb)
# NGCMX:年轻代(young)的最大容量 (kb)
# NGC:年轻代(young)中当前的容量 (kb)
# S0C:年轻代中第一个survivor(幸存区)的容量 (kb)
# S1C:年轻代中第二个survivor(幸存区)的容量 (kb)
# EC:年轻代中Eden(伊甸园)的容量 (kb)
# OGCMN:old代中初始化(最小)的大小 (kb)
# OGCMX:old代的最大容量 (kb)
# OGC:old代当前新生成的容量 (kb)
# OC:Old代的容量 (kb)
# PGCMN:perm代中初始化(最小)的大小 (kb)
# PGCMX:perm代的最大容量 (kb)
# PGC:perm代当前新生成的容量 (kb)
# PC:Perm(持久代)的容量 (kb)
# YGC:从应用程序启动到采样时年轻代中gc次数
# GCT:从应用程序启动到采样时gc用的总时间(s)

-gccause:垃圾收集统计概述(同-gcutil),附加最近两次垃圾回收事件的原因

[root@localhost ~]# jstat -gccause <pid>
  S0     S1     E      O      P     YGC     YGCT    FGC    FGCT     GCT    LGCC                 GCC       
  0.00  79.23  39.37  39.92  99.74      9    0.198     0    0.000    0.198 Allocation Failure   No GC
# 结果说明
##########################
# LGCC:最近垃圾回收的原因
# GCC:当前垃圾回收的原因

jstack

jstack(Java Stack Trace)主要用于打印线程的堆栈信息,是JDK自带的很强大的线程分析工具,可以帮助我们排查程序运行时的线程状态、死锁状态等。

# dump出进程<pid>的线程堆栈快照至/data/1.log文件中
jstack -l <pid> >/data/1.log

# 参数说明:
# -F:如果正常执行jstack命令没有响应(比如进程hung住了),可以加上此参数强制执行thread dump
# -m:除了打印Java的方法调用栈之外,还会输出native方法的栈帧
# -l:打印与锁有关的附加信息。使用此参数会导致JVM停止时间变长,在生产环境需慎用

jstack dump文件中值得关注的线程状态有:

  • 死锁(Deadlock) —— 重点关注
  • 执行中(Runnable)
  • 等待资源(Waiting on condition) —— 重点关注
    • 等待某个资源或条件发生来唤醒自己。具体需结合jstacktrace来分析,如线程正在sleep,网络读写繁忙而等待
    • 如果大量线程在“waiting on condition”,并且在等待网络资源,可能是网络瓶颈的征兆
  • 等待获取监视器(Waiting for monitor entry) —— 重点关注
    • 如果大量线程在“waiting for monitor entry”,可能是一个全局锁阻塞住了大量线程
  • 暂停(Suspended)
  • 对象等待中(Object.wait() 或 TIMED_WAITING)
  • 阻塞(Blocked) —— 重点关注
  • 停止(Parked)

注意:如果某个相同的call stack经常出现, 我们有80%的以上的理由确定这个代码存在性能问题(读网络的部分除外)。

场景一:分析BLOCKED问题

"RMI TCP Connection(267865)-172.16.5.25" daemon prio=10 tid=0x00007fd508371000 nid=0x55ae waiting for monitor entry [0x00007fd4f8684000]
   java.lang.Thread.State: BLOCKED (on object monitor)
at org.apache.log4j.Category.callAppenders(Category.java:201)
- waiting to lock <0x00000000acf4d0c0> (a org.apache.log4j.Logger)
at org.apache.log4j.Category.forcedLog(Category.java:388)
at org.apache.log4j.Category.log(Category.java:853)
at org.apache.commons.logging.impl.Log4JLogger.warn(Log4JLogger.java:234)
at com.tuan.core.common.lang.cache.remote.SpyMemcachedClient.get(SpyMemcachedClient.java:110)
  • 线程状态是 Blocked,阻塞状态。说明线程等待资源超时
  • “ waiting to lock <0x00000000acf4d0c0>”指,线程在等待给这个 0x00000000acf4d0c0 地址上锁(英文可描述为:trying to obtain 0x00000000acf4d0c0 lock)
  • 在 dump 日志里查找字符串 0x00000000acf4d0c0,发现有大量线程都在等待给这个地址上锁。如果能在日志里找到谁获得了这个锁(如locked < 0x00000000acf4d0c0 >),就可以顺藤摸瓜了
  • “waiting for monitor entry”说明此线程通过 synchronized(obj) {……} 申请进入了临界区,从而进入了下图1中的“Entry Set”队列,但该 obj 对应的 monitor 被其他线程拥有,所以本线程在 Entry Set 队列中等待
  • 第一行里,"RMI TCP Connection(267865)-172.16.5.25"是 Thread Name 。tid指Java Thread id。nid指native线程的id。prio是线程优先级。[0x00007fd4f8684000]是线程栈起始地址。

场景二:分析CPU过高问题

1.top命令找出最高占用的进程(Shift+P)

2.查看高负载进程下的高负载线程(top -Hp 或ps -mp -o THREAD,tid,time)

3.找出最高占用的线程并记录thread_id,把线程号进行换算成16进制编号(printf "%X\n" thread_id)

4.(可选)执行查看高负载的线程名称(jstack 16143 | grep 3fb6)

5.导出进程的堆栈日志,找到3fb6 这个线程号(jstack 16143 >/home/16143.log)

6.根据找到的堆栈信息关联到代码进行定位分析即可

jmap

jmap(Java Memory Map)主要用于打印内存映射。常用命令:

jmap -dump:live,format=b,file=xxx.hprof <pid>

查看JVM堆栈的使用情况

[root@localhost ~]# jmap -heap 7243
Attaching to process ID 27900, please wait...
Debugger attached successfully.
Client compiler detected.
JVM version is 20.45-b01
using thread-local object allocation.
Mark Sweep Compact GC
Heap Configuration: #堆内存初始化配置
   MinHeapFreeRatio = 40     #-XX:MinHeapFreeRatio设置JVM堆最小空闲比率  
   MaxHeapFreeRatio = 70   #-XX:MaxHeapFreeRatio设置JVM堆最大空闲比率  
   MaxHeapSize = 100663296 (96.0MB)   #-XX:MaxHeapSize=设置JVM堆的最大大小
   NewSize = 1048576 (1.0MB)     #-XX:NewSize=设置JVM堆的‘新生代’的默认大小
   MaxNewSize = 4294901760 (4095.9375MB) #-XX:MaxNewSize=设置JVM堆的‘新生代’的最大大小
   OldSize = 4194304 (4.0MB)  #-XX:OldSize=设置JVM堆的‘老生代’的大小
   NewRatio = 2    #-XX:NewRatio=:‘新生代’和‘老生代’的大小比率
   SurvivorRatio = 8  #-XX:SurvivorRatio=设置年轻代中Eden区与Survivor区的大小比值
   PermSize = 12582912 (12.0MB) #-XX:PermSize=<value>:设置JVM堆的‘持久代’的初始大小  
   MaxPermSize = 67108864 (64.0MB) #-XX:MaxPermSize=<value>:设置JVM堆的‘持久代’的最大大小  
Heap Usage:
New Generation (Eden + 1 Survivor Space): #新生代区内存分布,包含伊甸园区+1个Survivor区
   capacity = 30212096 (28.8125MB)
   used = 27103784 (25.848182678222656MB)
   free = 3108312 (2.9643173217773438MB)
   89.71169693092462% used
Eden Space: #Eden区内存分布
   capacity = 26869760 (25.625MB)
   used = 26869760 (25.625MB)
   free = 0 (0.0MB)
   100.0% used
From Space: #其中一个Survivor区的内存分布
   capacity = 3342336 (3.1875MB)
   used = 234024 (0.22318267822265625MB)
   free = 3108312 (2.9643173217773438MB)
   7.001809512867647% used
To Space: #另一个Survivor区的内存分布
   capacity = 3342336 (3.1875MB)
   used = 0 (0.0MB)
   free = 3342336 (3.1875MB)
   0.0% used
PS Old Generation: #当前的Old区内存分布
   capacity = 67108864 (64.0MB)
   used = 67108816 (63.99995422363281MB)
   free = 48 (4.57763671875E-5MB)
   99.99992847442627% used
PS Perm Generation: #当前的 “持久代” 内存分布
   capacity = 14417920 (13.75MB)
   used = 14339216 (13.674942016601562MB)
   free = 78704 (0.0750579833984375MB)
   99.45412375710227% used

新生代内存回收就是采用空间换时间方式;如果from区使用率一直是100% 说明程序创建大量的短生命周期的实例,使用jstat统计jvm在内存回收中发生的频率耗时以及是否有full gc,使用这个数据来评估一内存配置参数、gc参数是否合理。

统计一【jmap -histo】:统计所有类的实例数量和所占用的内存容量

[root@localhost ~]# jmap -histo 7243
 num     #instances         #bytes  class name
----------------------------------------------
   1:          8969       19781168  [B
   2:          1835        2296720  [I
   3:         19735        2050688  [C
   4:          3448         385608  java.lang.Class
   5:          3829         371456  [Ljava.lang.Object;
   6:         14634         351216  java.lang.String
   7:          6695         214240  java.util.concurrent.ConcurrentHashMap$Node
   8:          6257         100112  java.lang.Object
   9:          2155          68960  java.util.HashMap$Node
  10:           723          63624  java.lang.reflect.Method
  11:            49          56368  [Ljava.util.concurrent.ConcurrentHashMap$Node;
  12:           830          46480  java.util.zip.ZipFile$ZipFileInputStream
  13:          1146          45840  java.lang.ref.Finalizer
  ......

 统计二【jmap -histo】:查看实例数最多的对象,并过滤Map关键词,然后按降序排序输出

[root@localhost ~]# jmap -histo 7243 |grep Map|sort -k 2 -g -r|less
Total         96237       26875560
   7:          6695         214240  java.util.concurrent.ConcurrentHashMap$Node
   9:          2155          68960  java.util.HashMap$Node
  18:           563          27024  java.util.HashMap
  21:           505          20200  java.util.LinkedHashMap$Entry
  16:           337          34880  [Ljava.util.HashMap$Node;
  27:           336          16128  gnu.trove.THashMap
  56:           163           6520  java.util.WeakHashMap$Entry
  60:           127           6096  java.util.WeakHashMap
  38:           127          10144  [Ljava.util.WeakHashMap$Entry;
  53:           126           7056  java.util.LinkedHashMap
......

统计三【jmap -histo】:统计实例数量最多的前10个类

[root@localhost ~]# jmap -histo 7243 | sort -n -r -k 2 | head -10
 num     #instances         #bytes  class name
----------------------------------------------
Total         96237       26875560
   3:         19735        2050688  [C
   6:         14634         351216  java.lang.String
   1:          8969       19781168  [B
   7:          6695         214240  java.util.concurrent.ConcurrentHashMap$Node
   8:          6257         100112  java.lang.Object
   5:          3829         371456  [Ljava.lang.Object;
   4:          3448         385608  java.lang.Class
   9:          2155          68960  java.util.HashMap$Node
   2:          1835        2296720  [I

统计四【jmap -histo】:统计合计容量最多的前10个类

[root@localhost ~]# jmap -histo 7243 | sort -n -r -k 3 | head -10
 num     #instances         #bytes  class name
----------------------------------------------
Total         96237       26875560
   1:          8969       19781168  [B
   2:          1835        2296720  [I
   3:         19735        2050688  [C
   4:          3448         385608  java.lang.Class
   5:          3829         371456  [Ljava.lang.Object;
   6:         14634         351216  java.lang.String
   7:          6695         214240  java.util.concurrent.ConcurrentHashMap$Node
   8:          6257         100112  java.lang.Object
   9:          2155          68960  java.util.HashMap$Node

dump注意事项

  • 在应用快要发生FGC的时候把堆数据导出来

    ​ 老年代或新生代使用接近100%时,就表示即将发生GC,也可以在JVM参数中指定触发GC的阈值。

    • 查看快要发生FGC使用命令:jmap -heap < pid >
    • 数据导出:jmap -dump:format=b,file=heap.bin < pid >
  • 通过命令查看大对象:jmap -histo < pid >|less

使用总结

  • 如果程序内存不足或者频繁GC,很有可能存在内存泄露情况,这时候就要借助Java堆Dump查看对象的情况
  • 要制作堆Dump可以直接使用jvm自带的jmap命令
  • 可以先使用jmap -heap命令查看堆的使用情况,看一下各个堆空间的占用情况
  • 使用jmap -histo:[live]查看堆内存中的对象的情况。如果有大量对象在持续被引用,并没有被释放掉,那就产生了内存泄露,就要结合代码,把不用的对象释放掉
  • 也可以使用 jmap -dump:format=b,file=<fileName>命令将堆信息保存到一个文件中,再借助jhat命令查看详细内容
  • 在内存出现泄露、溢出或者其它前提条件下,建议多dump几次内存,把内存文件进行编号归档,便于后续内存整理分析
  • 在用cms gc的情况下,执行jmap -heap有些时候会导致进程挂起,因此强烈建议别执行这个命令,如果想获取内存目前每个区域的使用状况,可通过jstat -gc或jstat -gccapacity来拿到

jhat

jhat(JVM Heap Analysis Tool)命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看。在此要注意,一般不会直接在服务器上进行分析,因为jhat是一个耗时并且耗费硬件资源的过程,一般把服务器生成的dump文件复制到本地或其他机器上进行分析。

# 解析Java堆转储文件,并启动一个 web server
jhat heapDump.dump

jconsole

jconsole(Java Monitoring and Management Console)是一个Java GUI监视工具,可以以图表化的形式显示各种数据,并可通过远程连接监视远程的服务器VM。用java写的GUI程序,用来监控VM,并可监控远程的VM,非常易用,而且功能非常强。命令行里打jconsole,选则进程就可以了。

第一步:在远程机的tomcat的catalina.sh中加入配置:

JAVA_OPTS="$JAVA_OPTS -Djava.rmi.server.hostname=192.168.202.121 -Dcom.sun.management.jmxremote"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.port=12345"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.authenticate=true"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.ssl=false"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.pwd.file=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.101-3.b13.el7_2.x86_64/jre/lib/management/jmxremote.password"

第二步:配置权限文件

[root@localhost bin]# cd /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.101-3.b13.el7_2.x86_64/jre/lib/management/
[root@localhost management]# cp jmxremote.password.template jmxremote.password
[root@localhost management]# vi jmxremote.password

 jmxremote.password文件内容如下(文件内容为每行一个用户名和对应的密码,中间用空格或制表符分隔):

monitorRole QED
controlRole chenqimiao

第三步:配置权限文件为600

[root@localhost management]# chmod 600 jmxremote.password jmxremote.access

这样基本配置就结束了,下面说两个坑,第一个就是防火墙的问题,要开放指定端口的防火墙,我这里配置的是12345端口,第二个是hostname的问题:

jconsole-ip2

请将127.0.0.1修改为本地真实的IP,我的服务器IP是192.168.202.121:

jconsole-ip

第四步:查看JConsole

JConsole-新建连接

JConsole-Console

jvisualvm

jvisualvm(JVM Monitoring/Troubleshooting/Profiling Tool)同jconsole都是一个基于图形化界面的、可以查看本地及远程的JAVA GUI监控工具,Jvisualvm同jconsole的使用方式一样,直接在命令行输入Jvisualvm即可启动,不过Jvisualvm相比,界面更美观一些,数据更实时。 jvisualvm的使用VisualVM进行远程连接的配置和JConsole是一摸一样的,最终效果图如下

Visual GC(监控垃圾回收器)

Java VisualVM默认没有安装Visual GC插件,需要手动安装,JDK的安装目录的bin目录下双击 jvisualvm.sh,即可打开Java VisualVM,点击菜单栏: 工具->插件 安装Visual GC,最终效果如下图所示:

大dump文件

从服务器dump堆内存后文件比较大(5.5G左右),加载文件、查看实例对象都很慢,还提示配置xmx大小。表明给VisualVM分配的堆内存不够,找到$JAVA_HOME/lib/visualvm}/etc/visualvm.conf这个文件,修改:

default_options="-J-Xms24m -J-Xmx192m"

再重启VisualVM就行了。

jmc

jmc(Java Mission Control)是JDK自带的一个图形界面监控工具,监控信息非常全面。JMC打开性能日志后,主要包括一般信息、内存、代码、线程、I/O、系统、事件 功能。

jmc-main

JMC的最主要的特征就是JFR(Java Flight Recorder),是基于JAVA的飞行记录器,JFR的数据是一些JVM事件的历史纪录,可以用来诊断JVM的性能和操作,收集后的数据可以使用JMC来分析。

启动JFR

在商业版本上面,JFR默认是关闭的,可以通过在启动时增加参数 -XX:+UnlockCommercialFeatures -XX:+FlightRecorder 来启动应用。启动之后,也只是开启了JFR特性,但是还没有开始进行事件记录。这就要通过GUI和命令行了。

  • 通过Java Mission Control启动JFR

    打开Java Mission Control点击对应的JVM启动即可,事件记录有两种模式(如果选择第2种模式,那么JVM会使用一个循环使用的缓存来存放事件数据):

    • 记录固定一段时间的事件(比如:1分钟)
    • 持续进行记录
  • 通过命令行启动JFR

    通过在启动的时候,增加参数:-XX:+FlightRecorderOptions=string 来启动真正地事件记录,这里的 string 可以是以下值(下列参数都可以使用jcmd命令,在JVM运行的时候进行动态调整,参考地址):

    • name=name:标识recording的名字(一个进程可以有多个recording存在,它们使用名字来进行区分)
    • defaultrecording=<ture|false>:是否启动recording,默认是false,我们要分析,必须要设置为true
    • setting=paths:包含JFR配置的文件名字
    • delay=time:启动之后,经过多长时间(比如:30s,1h)开始进行recording
    • duration=time:做多长时间的recording
    • filename=path:recordding记录到那个文件里面
    • compress=<ture|false>:是否对recording进行压缩(gzip),默认为false
    • maxage=time:在循环使用的缓存中,事件数据保存的最大时长
    • maxsize=size:事件数据缓存的最大大小(比如:1024k,1M)

常用JFR命令

  • 启动recording

    命令格式:jcmd process_id JFR.start [options_list],其中options_list就是上述的参数值。

  • dump出循环缓存中的数据

    命令格式:jcmd process_id JFR.dump [options_list],其中options_list参数的可选值如下:

    • name=name:recording的名字
    • recording=n:JFR recording的数字(一个标识recording的随机数)
    • filename=path:dump文件的保存路径
  • 查看进程中所有recording

    命令格式: jcmd process_id JFR.check [verbose],不同recording使用名字进行区分,同时JVM还为它分配一个随机数。

  • 停止recording

    命令格式: jcmd process_id JFR.stop [options_list],其中options_list参数的可选值如下:

    • name=name:要停止的recording名字
    • recording=n:要停止的recording的标识数字
    • discard=boolean:如果为true,数据被丢弃,而不是写入下面指定的文件当中
    • filename=path:写入数据的文件名称

命令启动JFR案例

  • 第一步:创建一个包含了你自己配置的JFR模板文件(template.jfc)。运行jmc, 然后Window->Flight Recording Template Manage菜单。准备好档案后,就可以导出文件,并移动到要排查问题的环境中

    jmc-jfc

  • 第二步:由于JFR需要JDK的商业证书,这一步需要解锁jdk的商业特性

    [root@localhost bin]# jcmd 12234 VM.unlock_commercial_features
    12234: Commercial Features already unlocked.
    
  • 第三步:最后你就可以启动JFR,命令格式如下:

    jcmd <PID> JFR.start name=test duration=60s [settings=template.jfc] filename=output.jfr
    

    ​ 上述命令立即启动JFR并开始使用 template.jfc(在 $JAVA_HOME/jre/lib/jfr 下有 default.jfc 和 profile.jfc 模板)的配置收集 60s 的JVM信息,输出到 output.jfr 中。一旦记录完成之后,就可以复制.jfr文件到你的工作环境使用jmc GUI来分析。它几乎包含了排查jvm问题需要的所有信息,包括堆dump时的异常信息。使用案例如下:

    [root@localhost bin]# jcmd 12234 JFR.start name=test duration=60s filename=output.jfr
    12234: Started recording 6. The result will be written to: /root/zookeeper-3.4.12/bin/output.jfr
    [root@localhost bin]# ls -l
    -rw-r--r-- 1 root root 298585 6月  29 11:09 output.jfr
    

JFR(Java Flight Recorder)

  • Java Mission Control的最主要的特征就是Java Flight Recorder。正如它的名字所示,JFR的数据是一些列JVM事件的历史纪录,可以用来诊断JVM的性能和操作
  • JFR的基本操作就是开启哪些事件(比如:线程由于等待锁而阻塞的事件)。当开启的事件发生了,事件相关的数据会记录到内存或磁盘文件上。记录事件数据的缓存是循环使用的,只有最近发生的事件才能够从缓存中找到,之前的都因为缓存的限制被删除了。Java Mission Control能够对这些事件在界面上进行展示(从JVM的内存中读取或从事件数据文件中读取),我们可以通过这些事件来对JVM的性能进行诊断
  • 事件的类型、缓存的大小、事件数据的存储方式等等都是通过JVM参数、Java Mission Control的GUI界面、jcmd命令来控制的。JFR默认是编译进程序的,因为它的开销很小,一般来说对应用的影响小于1%。不过,如果我们增加了事件的数目、修改了记录事件的阈值,都有可能增加JFR的开销

JFR概况

​ 下面对GlassFish web服务器进行JFR记录的例子,在这个服务器上面运行着在第2章介绍的股票servlet。Java Mission Control加载完JFR获取的事件之后,大概是下面这个样子:

jfr-main

我们可以看到,通过上图可以看到:CPU使用率,Heap使用率,JVM信息,System Properties,JFR的记录情况等等。

JFR内存视图

Java Mission Control 可以看到非常多的信息,下图只显示了一个标签的内容。下图显示了JVM 的内存波动非常频繁,因为新生代经常被清除(有意思的是,head的大小并没有增长)。下面左边的面板显示了最近一段时间的垃圾回收情况,包括:GC的时长和垃圾回收的类型。如果我们点击一个事件,右边的面板会展示这个事件的具体情况,包括:垃圾垃圾回收的各个阶段及其统计信息。从面板的标签可以看到,还有很多其它信息,比如:有多少对象被清除了,花了多长时间;GC算法的配置;分配的对象信息等等。在第5章和第6章中,我们会详细介绍。

jfr-memory

JFR 代码视图

这张图也有很多tab,可以看到各个包的使用频率和类的使用情况、异常、编译、代码缓存、类加载情况等等:

jfr-code

JFR事件视图

下图显示了事件的概述视图:

jfr-event

EclipseMAT

虽然Java虚拟机可以帮我们对内存进行回收,但是其回收的是Java虚拟机不再引用的对象。很多时候我们使用系统的IO流、Cursor、Receiver如果不及时释放,就会导致内存泄漏(OOM)。但是,很多时候内存泄漏的现象不是很明显,比如内部类、Handler相关的使用导致的内存泄漏,或者你使用了第三方library的一些引用,比较消耗资源,但又不是像系统资源那样会引起你足够的注意去手动释放它们。以下通过内存泄漏分析、集合使用率、Hash性能分析和OQL快读定位空集合来使用MAT。

GC Roots

JAVA虚拟机通过可达性(Reachability)来判断对象是否存活,基本思想:以”GC Roots”的对象作为起始点向下搜索,搜索形成的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(即不可达的),则该对象被判定为可以被回收的对象,反之不能被回收。GC Roots可以是以下任意对象

  • 一个在current thread(当前线程)的call stack(调用栈)上的对象(如方法参数和局部变量)
  • 线程自身或者system class loader(系统类加载器)加载的类
  • native code(本地代码)保留的活动对象

内存泄漏

当对象无用了,但仍然可达(未释放),垃圾回收器无法回收。

Java四种引用类型

  • Strong References(强引用)

    普通的java引用,我们通常new的对象就是:StringBuffer buffer = new StringBuffer(); 如果一个对象通过一串强引用链可达,那么它就不会被垃圾回收。你肯定不希望自己正在使用的引用被垃圾回收器回收吧。但对于集合中的对象,应在不使用的时候移除掉,否则会占用更多的内存,导致内存泄漏。

  • Soft Reference(软引用)

    当对象是Soft Reference可达时,gc会向系统申请更多内存,而不是直接回收它,当内存不足的时候才回收它。因此Soft Reference适合用于构建一些缓存系统,比如图片缓存。

  • Weak Reference(弱引用)

    WeakReference不会强制对象保存在内存中。它拥有比较短暂的生命周期,允许你使用垃圾回收器的能力去权衡一个对象的可达性。在垃圾回收器扫描它所管辖的内存区域过程中,一旦gc发现对象是Weak Reference可达,就会把它放到 Reference Queue 中,等下次gc时回收它。

    系统为我们提供了WeakHashMap,和HashMap类似,只是其key使用了weak reference。如果WeakHashMap的某个key被垃圾回收器回收,那么entity也会自动被remove。由于WeakReference被GC回收的可能性较大,因此,在使用它之前,你需要通过weakObj.get()去判断目的对象引用是否已经被回收。一旦WeakReference.get()返回null,它指向的对象就会被垃圾回收,那么WeakReference对象就没有用了,意味着你应该进行一些清理。比如在WeakHashMap中要把回收过的key从Map中删除掉,避免无用的的weakReference不断增长。

    ReferenceQueue可以让你很容易地跟踪dead references。WeakReference类的构造函数有一个ReferenceQueue参数,当指向的对象被垃圾回收时,会把WeakReference对象放到ReferenceQueue中。这样,遍历ReferenceQueue可以得到所有回收过的WeakReference。

  • Phantom Reference(虚引用)

    其余Soft/Weak Reference区别较大是它的get()方法总是返回null。这意味着你只能用Phantom Reference本身,而得不到它指向的对象。当Weak Reference指向的对象变得弱可达(weakly reachable)时会立即被放到ReferenceQueue中,这在finalization、garbage collection之前发生。理论上,你可以在finalize()方法中使对象“复活”(使一个强引用指向它就行了,gc不会回收它)。但没法复活PhantomReference指向的对象。而PhantomReference是在garbage collection之后被放到ReferenceQueue中的,没法复活。

MAT视图与概念

  • Shallow Heap

    Shallow Size就是对象本身占用内存的大小,不包含其引用的对象内存,实际分析中作用不大。 常规对象(非数组)的Shallow Size由其成员变量的数量和类型决定。数组的Shallow Size有数组元素的类型(对象类型、基本类型)和数组长度决定。案例如下:

public class String {
    public final class String {8 Bytes header
    private char value[]; 4 Bytes
    private int offset; 4 Bytes
    private int count; 4 Bytes
    private int hash = 0; 4 Bytes
	// ......
}
// "Shallow size“ of a String ==24 Bytes12345678

Java的对象成员都是些引用。真正的内存都在堆上,看起来是一堆原生的byte[]、char[]、int[],对象本身的内存都很小。所以我们可以看到以Shallow Heap进行排序的Histogram图中,排在第一位第二位的是byte和char。

  • Retained Heap

    Retained Heap值的计算方式是将Retained Set中的所有对象大小叠加。或者说,由于X被释放,导致其它所有被释放对象(包括被递归释放的)所占的Heap大小。当X被回收时哪些将被GC回收的对象集合。比如:

    一个ArrayList持有100000个对象,每一个占用16 bytes,移除这些ArrayList可以释放16×100000+X,X代表ArrayList的Shallow大小。相对于Shallow Heap,Retained Heap可以更精确的反映一个对象实际占用的大小(因为如果该对象释放,Retained Heap都可以被释放)。

  • Histogram

    可列出每一个类的实例数。支持正则表达式查找,也可以计算出该类所有对象的Retained Size。

mat-histogram

  • Dominator Tree

    对象之间dominator关系树。如果从GC Root到达Y的的所有path都经过X,那么我们称X dominates Y,或者X是Y的Dominator Dominator Tree由系统中复杂的对象图计算而来。从MAT的dominator tree中可以看到占用内存最大的对象以及每个对象的dominator。 我们也可以右键选择Immediate Dominator”来查看某个对象的dominator。

mat-dominator-tree

  • Path to GC Roots

    ​ 查看一个对象到RC Roots的引用链通常在排查内存泄漏的时候,我们会选择exclude all phantom/weak/soft etc.references, 意思是查看排除虚引用/弱引用/软引用等的引用链,因为被虚引用/弱引用/软引用的对象可以直接被GC给回收,我们要看的就是某个对象否还存在Strong 引用链(在导出HeapDump之前要手动出发GC来保证),如果有,则说明存在内存泄漏,然后再去排查具体引用。

    mat-path-to-gc-roots

    查看当前Object所有引用,被引用的对象:

    • List objects with (以Dominator Tree的方式查看)
      • incoming references 引用到该对象的对象
      • outcoming references 被该对象引用的对象
    • Show objects by class (以class的方式查看)
      • incoming references 引用到该对象的对象
      • outcoming references 被该对象引用的对象
  • OQL(Object Query Language)

类似SQL查询语言:Classes:Table、Objects:Rows、Fileds:Cols

select * from com.example.mat.Listener
# 查找size=0并且未使用过的ArrayList
select * from java.util.ArrayList where size=0 and modCount=01
# 查找所有的Activity 
select * from instanceof android.app.Activity
  • 内存快照对比

方式一:Compare To Another Heap Dump(直接进行比较)

mat-compare-to-another-heap-dump-1

mat-compare-to-another-heap-dump-2

mat-compare-to-another-heap-dump-3

方式二:Compare Baseket(更全面,可以直接给出百分比)

mat-compare-baseket-1

mat-compare-baseket-2

mat-compare-baseket-3

mat-compare-baseket-4

MAT内存分析实战

  • 实战一:内存泄漏分析

    查找导致内存泄漏的类。既然环境已经搭好,heap dump也成功倒入,接下来就去分析问题。

    • 查找目标类 如果在开发过程中,你的目标很明确,比如就是查找自己负责的服务,那么通过包名或者Class筛选,OQL搜索都可以快速定位到。点击OQL图标,在窗口输入,并按Ctrl + F5或者!按钮执行:

      select * from instanceof android.app.Activity

    • Paths to GC Roots:exclude all phantom/weak/soft etc.references

      查看一个对象到RC Roots是否存在引用链。要将虚引用/弱引用/软引用等排除,因为被虚引用/弱引用/软引用的对象可以直接被GC给回收

    • 分析具体的引用为何没有被释放,并进行修复

小技巧:

  • 当目的不明确时,可以直接定位到RetainedHeap最大的Object,Select incoming references,查看引用链,定位到可疑的对象然后Path to GC Roots进行引用链分析
  • 如果大对象筛选看不出区别,可以试试按照class分组,再寻找可疑对象进行GC引用链分析
  • 直接按照包名直接查看GC引用链,可以一次性筛选多个类,但是如下图所示,选项是 Merge Shortest Path to GCRoots,这个选项具体不是很明白,不过也能筛选出存在GC引用链的类,这种方式的准确性还待验证

mat-实践一

所以有时候进行MAT分析还是需要一些经验,能够帮你更快更准确的定位。

  • 实战二:集合使用率分析

    集合在开发中会经常使用到,如何选择合适的数据结构的集合,初始容量是多少(太小,可能导致频繁扩容),太大,又会开销跟多内存。当这些问题不是很明确时或者想查看集合的使用情况时,可以通过MAT来进行分析。

    • 筛选目标对象

      mat-实践二-1

    • Show Retained Set(查找当X被回收时那些将被GC回收的对象集合)

      mat-实践二-2

    • 筛选指定的Object(Hash Map,ArrayList)并按照大小进行分组

      mat-实践二-3

    • 查看指定类的Immediate dominators

      mat-实践二-4

Collections fill ratio

这种方式只能查看那些具有预分配内存能力的集合,比如HashMap,ArrayList。计算方式:”size / capacity”

mat-实践二-5

mat-实践二-6

  • 实战三:Hash相关性能分析

    当Hash集合中过多的对象返回相同Hash值的时候,会严重影响性能(Hash算法原理自行搜索),这里来查找导致Hash集合的碰撞率较高的罪魁祸首。

    • Map Collision Ratio

      检测每一个HashMap或者HashTable实例并按照碰撞率排序:碰撞率 = 碰撞的实体/Hash表中所有实体

      mat-实践三-1

    • 查看Immediate dominators

      mat-实践三-2

      mat-实践三-3

    • 通过HashEntries查看key value

      mat-实践三-4

    • Array等其它集合分析方法类似

  • 实战四:通过OQL快速定位未使用的集合

    • 通过OQL查询empty并且未修改过的集合:

      select * from java.util.ArrayList where size=0 and modCount=01
      select * from java.util.HashMap where size=0 and modCount=0
      select * from java.util.Hashtable where count=0 and modCount=012
      

      mat-实践四-1

    • Immediate dominators(查看引用者)

      mat-实践四-2

    • 计算空集合的Retained Size值,查看浪费了多少内存

      mat-实践四-3

🔥火焰图

火焰图是用来分析程序运行瓶颈的工具。火焰图也可以用来分析 Java 应用。

环境安装

确认你的机器已经安装了git、jdk、perl、c++编译器

安装Perl
wget http://www.cpan.org/src/5.0/perl-5.26.1.tar.gz
tar zxvf perl-5.26.1.tar.gz
cd perl-5.26.1
./Configure -de
make
make test
make install

wget后面的路径可以按需更改。安装过程比较耗时间,安装完成后可通过perl -version查看是否安装成功。

C++编译器
apt-get install g++

一般用于编译c++程序,缺少这个编译器进行make编译c++代码时,会报“g++: not found”的错误。

clone相关项目

下载下来所需要的两个项目(这里建议放到data目录下):

git clone https://github.com/jvm-profiling-tools/async-profiler
git clone https://github.com/brendangregg/FlameGraph
编译项目

下载好以后,需要打开async-profiler文件,输入make指令进行编译:

cd async-profiler
make

生成文件

生成火焰图数据

可以从 github 上下载 async-profiler 的压缩包进行相关操作。进入async-profiler项目的目录下,然后输入如下指令:

./profiler.sh -d 60 -o collapsed -f /tmp/test_01.txt ${pid}

上面的-d表示的是持续时长,后面60代表持续采集时间60s,-o表示的是采集规范,这里用的是collapsed,-f后面的路径,表示的是数据采集后生成的数据存放的文件路径(这里放在了/tmp/test_01.txt),${pid}表示的是采集目标进程的pid,回车运行,运行期间阻塞,知道约定时间完成。运行完成后去tmp下看看有没有对应文件。

生成svg文件

上一步产生的文件里的内容,肉眼是很难看懂的,所以现在FlameGraph的作用就体现出来了,它可以读取该文件并生成直观的火焰图,现在进入该项目目录下面,执行如下语句:

perl flamegraph.pl --colors=java /tmp/test_01.txt > test_01.svg

因为是perl文件,这里使用perl指令运行该文件,后面--colors表示着色风格,这里是java,后面的是数据文件的路径,这里是刚刚上面生成的那个文件/tmp/test_01.txt,最后的test_01.svg就是最终生成的火焰图文件存放的路径和文件命名,这里是命名为test_01.svg并保存在当前路径,运行后看到该文件已经存在于当前目录下。

展示火焰图

现在下载下来该文件,使用浏览器打开,效果如下:

火焰图案例

生成的火焰图案例如下:

火焰图案例

瓶颈点1

CoohuaAnalytics$KafkaConsumer:::send方法中Gzip压缩占比较高。已经定位到方法级别,再看代码就快速很多,直接找到具体位置,找到第一个消耗大户:Gzip压缩

火焰图-瓶颈点1

瓶颈点2

展开2这个波峰,查看到这个getOurStackTrace方法占用了大比例的CPU,怀疑代码里面频繁用丢异常的方式获取当前代码栈:

火焰图-瓶颈点2

直接看代码:

火焰图-瓶颈点2-代码

果然如推断,找到第二个CPU消耗大户:new Exception().getStackTrace()。

瓶颈点3

展开波峰3,可以看到是这个Gzip解压缩:

火焰图-瓶颈点3

定位到具体的代码,可以看到对每个请求的参数进行了gzip解压缩:

火焰图-瓶颈点3-代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值