当前位置:网站首页 > Java基础 > 正文

jvisualvm分析dump文件 定位大对象(java visualvm分析 dump)



作者:盛开的太阳

Arthas线上

分析诊断调优工具

以前我们要排查线上问题,通常使用的是jdk自带的调优工具和命令。最常见的就是dump线上日志,然后下载到本地,导入到jvisualvm工具中。这样操作有诸多不变,现在阿里团队开发的Arhtas工具,拥有非常强大的功能,并且都是线上的刚需,尤其是情况紧急,不方便立刻发版,适合临时处理危急情况使用。下面分两部分来研究JVM性能调优工具:

1.JDK自带的性能调优工具

虽然有了Arthas,但也不要忘记JDK自带的性能调优工具,在某些场景下,他还是有很大作用的。而且Arthas里面很多功能其根本就是封装了JDK自带的这些调优命令。

2.Arthas线上分析工具的使用

这一部分,主要介绍几个排查线上问题常用的方法。功能真的很强大,刚兴趣的猿媛可以研究其基本原理。之前跟我同事讨论,感觉这就像病毒一样,可以修改内存里的东西,真的还是挺强大的。

以上两种方式排查线上问题,没有优劣之分,如果线上不能安装Arthas就是jdk自带命令,如果jdk自带命令不能满足部分要求,又可以安装Arthas,那就使用Arthas。他们只是排查问题的工具,重要的是排查问题的思路。不管黑猫、白猫,能抓住耗子就是好猫。

这里不是流水一样的介绍功能怎么用,就说说线上遇到的问题,我们通常怎么排查,排查的几种情况。

  • 内存溢出,出现OutOfMemoryError,这个问题如何排查
  • CPU使用猛增,这个问题如何排查?
  • 进程有死锁,这个问题如何排查?
  • JVM参数调优

下面来一个一个解决

使用的命令:

 
  

运行结果:

通过这个命令,我们可以看出当前哪个对象最消耗内存。

上面这个运行结果是我启动了本地的一个项目,然后运行【jmap -histro 进程号】运行出来的结果,直接去了其中的一部分。通过这里我们可以看看大的实例对象中,有没有我们自定义的实例对象。通过这个可以排查出哪个实例对象引起的内存溢出。

除此之外,Total汇总数据可以看出当前一共有多少个对象,暂用了多大内存空间。这里是有约860w个对象,占用约923M的空间。

使用命令

 
  

比如,我本地启动了一个项目,想要查看这个项目的内存占用情况:

 
  
下面来看看参数的含义


堆空间配置信息

G1堆使用情况

 
  
G1年轻代Eden区使用情况

G1年轻代Survivor区使用情况和G1老年代使用情况:和Eden区类似
 
  

通过上面的命令,我们就能知道当前系统堆空间的使用情况了,到底是老年代有问题还是新生代有问题。

如果前两种方式还是没有排查出问题,我们可以导出内存溢出的日志,在导入客户端进行分析

使用的命令是:

或者是直接设置JVM参数

然后导入到jvisualvm中进行分析,方法是:点击文件->装入,导入文件,查看系统的运行情况了。

通过分析实例数,看看哪个对象实例占比最高,这里重点看我们自定义的类,然后分析这个对象里面有没有大对象,从而找出引起内存溢出的根本原因。

我们可以通过Jstack找出占用cpu最高的线程的堆栈信息,下面来一步一步分析。

假设我们有一段死循环,不断执行方法调用,线程始终运行不释放就会导致CPU飙高,示例代码如下:

如上,现在有一个java进程,cpu严重飙高了,接下来如何处理呢?

我们看到了单独的46518这个线程的详细信息

需要注意的是,这里的H是大写的H。

我们可以看出线程0和线程1线程号飙高。

通过上图我们看到占用cpu资源最高的线程有两个,线程号分别是,。我们一第一个为例说明,如何查询这个线程是哪个线程,以及这个线程的什么地方出现问题,导致cpu飙高。

是线程号为的十六进制数。具体转换可以网上查询工具。

接下来查询飙高线程的堆栈信息

 
  
  • :表示的是进程号
  • : 表示的是线程号对应的十六进制数

通过这个方式可以查询到这个线程对应的堆栈信息

从这里我们可以看出有问题的线程id是0x4cd0, 哪一句代码有问题呢,Math类的22行。

上述方法定位问题已经很精确了,接下来就是区代码里排查为什么会有问题了。

备注:上面的进程id可能没有对应上,在测试的时候,需要写对进程id和线程id

Jstack可以用来查看堆栈使用情况,以及进程死锁情况。下面就来看看如何排查进程死锁

还是通过案例来分析

 
  

上面是两把锁,互相调用。

  1. 定义了两个成员变量lock1,lock2
  2. main方法中定义了两个线程。
    • 线程1内部使用的是同步执行--上锁,锁是lock1。休眠5秒钟之后,他要获取第二把锁,执行第二段代码。
    • 线程2和线程1类似,锁相反。
  3. 问题:一开始,像个线程并行执行,线程一获取lock1,线程2获取lock2.然后线程1继续执行,当休眠5s后获取开启第二个同步执行,锁是lock2,但这时候很可能线程2还没有执行完,所以还没有释放lock2,于是等待。线程2刚开始获取了lock2锁,休眠五秒后要去获取lock1锁,这时lock1锁还没释放,于是等待。两个线程就处于相互等待中,造成死锁。
 
  

从这里面个异常可以看出,

  • prio:当前线程的优先级
  • cpu:cpu耗时
  • os_prio:操作系统级别的优先级
  • tid:线程id
  • nid:系统内核的id
  • state:当前的状态,BLOCKED,表示阻塞。通常正常的状态是Running我们看到Thread-0和Thread-1线程的状态都是BLOCKED.

通过上面的信息,我们判断出两个线程的状态都是BLOCKED,可能有点问题,然后继续往下看。

我们从最后的一段可以看到这句话:Found one Java-level deadlock; 意思是找到一个死锁。死锁的线程号是Thread-0,Thread-1。

Thread-0:正在等待0x000000070e706ef8对象的锁,这个对象现在被Thread-1持有。

Thread-1:正在等待0x000000070e705c98对象的锁,这个对象现在正在被Thread-0持有。

最下面展示的是死锁的堆栈信息。死锁可能发生在DeadLockTest的第17行和第31行。通过这个提示,我们就可以找出死锁在哪里了。

如果使用jstack感觉不太方便,还可以使用jvisualvm,通过界面来查看,更加直观。

在程序代码启动的过程中,打开jvisualvm工具。

找到当前运行的类,查看线程,就会看到最头上的一排红字:检测到死锁。然后点击“线程Dump”按钮,查看相信的线程死锁的信息。

这里可以找到线程私锁的详细信息,具体内容和上面使用Jstack命令查询的结果一样,这里实用工具更加方便。

jvm调优通常使用的是Jstat命令。

 
  

这个命令非常常用,在线上有问题的时候,可以通过这个命令来分析问题。

下面我们来测试一下,启动一个项目,然后在终端驶入jstat -gc 进程id,得到如下结果:

上面的参数分别是什么意思呢?先识别参数的含义,然后根据参数进行分析

  • S0C: 第一个Survivor区的容量
  • S1C: 第二个Survivor区的容量
  • S0U: 第一个Survivor区已经使用的容量
  • S1U:第二个Survivor区已经使用的容量
  • EC: 新生代Eden区的容量
  • EU: 新生代Eden区已经使用的容量
  • OC: 老年代容量
  • OU:老年代已经使用的容量
  • MC: 方法区大小(元空间)
  • MU: 方法区已经使用的大小
  • CCSC:压缩指针占用空间
  • CCSU:压缩指针已经使用的空间
  • YGC: YoungGC已经发生的次数
  • YGCT: 这一次YoungGC耗时
  • FGC: Full GC发生的次数
  • FGCT: Full GC耗时
  • GCT: 总的GC耗时,等于YGCT+FGCT

连续观察GC变化的命令

 
  

举个例子:我要打印10次gc信息,每次间隔1秒

jstat -gc 进程ID 1000 10

这样就连续打印了10次gc的变化,每次隔一秒。

这个命令是对整体垃圾回收情况的统计,下面将会差分处理。

这个命令是打印堆内存的使用情况。

 
  

  • NGCMN:新生代最小容量
  • NGCMX:新生代最大容量
  • NGC:当前新生代容量
  • S0C:第一个Survivor区大小
  • S1C:第二个Survivor区大小
  • EC:Eden区的大小
  • OGCMN:老年代最小容量
  • OGCMX:老年代最大容量
  • OGC:当前老年代大小
  • OC: 当前老年代大小
  • MCMN: 最小元数据容量
  • MCMX:最大元数据容量
  • MC:当前元数据空间大小
  • CCSMN:最小压缩类空间大小
  • CCSMX:最大压缩类空间大小
  • CCSC:当前压缩类空间大小
  • YGC:年轻代gc次数
  • FGC:老年代GC次数

命令:

 
  

这个指的是当前某一次GC的内存情况

  • S0C:第一个Survivor的大小
  • S1C:第二个Survivor的大小
  • S0U:第一个Survivor已使用大小
  • S1U:第二个Survivor已使用大小
  • TT: 对象在新生代存活的次数
  • MTT: 对象在新生代存活的最大次数
  • DSS: 期望的Survivor大小
  • EC:Eden区的大小
  • EU:Eden区的使用大小
  • YGC:年轻代垃圾回收次数
  • YGCT:年轻代垃圾回收消耗时间
 
  

参数含义:

  • NGCMN:新生代最小容量
  • NGCMX:新生代最大容量
  • NGC:当前新生代容量
  • S0CMX:Survivor 1区最大大小
  • S0C:当前Survivor 1区大小
  • S1CMX:Survivor 2区最大大小
  • S1C:当前Survivor 2区大小
  • ECMX:最大Eden区大小
  • EC:当前Eden区大小
  • YGC:年轻代垃圾回收次数
  • FGC:老年代回收次数

命令:

 
  

参数含义:

  • MC:方法区大小
  • MU:方法区已使用大小
  • CCSC:压缩指针类空间大小
  • CCSU:压缩类空间已使用大小
  • OC:老年代大小
  • OU:老年代已使用大小
  • YGC:年轻代垃圾回收次数
  • FGC:老年代垃圾回收次数
  • FGCT:老年代垃圾回收消耗时间
  • GCT:垃圾回收消耗总时间,新生代+老年代

命令:

 
  

参数含义:

  • OGCMN:老年代最小容量
  • OGCMX:老年代最大容量
  • OGC:当前老年代大小
  • OC:老年代大小
  • YGC:年轻代垃圾回收次数
  • FGC:老年代垃圾回收次数
  • FGCT:老年代垃圾回收消耗时间
  • GCT:垃圾回收消耗总时间

命令

 
  

  • MCMN:最小元数据容量
  • MCMX:最大元数据容量
  • MC:当前元数据空间大小
  • CCSMN:最小指针压缩类空间大小
  • CCSMX:最大指针压缩类空间大小
  • CCSC:当前指针压缩类空间大小
  • YGC:年轻代垃圾回收次数
  • FGC:老年代垃圾回收次数
  • FGCT:老年代垃圾回收消耗时间
  • GCT:垃圾回收消耗总时间

命令:

 
  

  • S0:Survivor 1区当前使用比例
  • S1:Survivor 2区当前使用比例
  • E:Eden区使用比例
  • O:老年代使用比例
  • M:元数据区使用比例
  • CCS:指针压缩使用比例
  • YGC:年轻代垃圾回收次数
  • YGCT:年轻代垃圾回收消耗时间
  • FGC:老年代垃圾回收次数
  • FGCT:老年代垃圾回收消耗时间
  • GCT:垃圾回收消耗总时间

通过查询上面的参数来分析整个堆空间。

Arthas的功能非常强大,现附上官方文档:https://arthas.aliyun.com/doc/

其实想要了解Arthas,看官方文档就可以了,功能全而详细。那为什么还要整理一下呢?我们这里整理的是一些常用功能,以及在紧急情况下可以给我们帮大忙的功能。

Arthas分为几个部分来研究,先来看看我们的研究思路哈

1.安装及启动---这一块简单看,对于程序员来说,so easy

2.dashboard仪表盘功能---类似于JDK的jstat命令,

3.thread命令查询进行信息---类似于jmap命令

4.反编译线上代码----这个功能很牛,改完发版了,怎么没生效,反编译看看。

5.查询某一个函数的返回值

6.查询jvm信息,并修改----当发生内存溢出是,可以手动设置打印堆日志到文件

7.profiler火焰图

下面就来看看Arthas的常用功能的用法吧

其实说到这快,不得不提的是,之前我一直因为arthas是一个软件,要启动,界面操作。当时我就想,要是这样,在线上安装一个单独的应用,公司肯定不同意啊~~~,研究完才发现,原来Arthas就是一个jar包。运行起来就是用java -jar 就可以。

可以直接在Linux上通过命令下载:

 
  

也可以在浏览器直接访问https://alibaba.github.io/arthas/arthas-boot.jar,等待下载成功后,上传到Linux服务器上。

执行命令就可以启动了

 
  

启动成功可以看到如下界面:

然后找到你想监控的进程,输入前面对应的编号,就可以开启进行监控模式了。比如我要看4

看到这个就表示,进入应用监听成功

执行命令

 
  

这里面一共有三块

我们可以看到当前进程下所有的线程信息。其中第13,14号线程当前处于BLOCKED阻塞状态,阻塞时间也可以看到。通过这个一目了然,当前有两个线程是有问题的,处于阻塞状态GC线程有6个。

内存信息包含三个部分:堆空间信息、非堆空间信息和GC垃圾收集信息

堆空间信息

  • g1_eden_space: Eden区空间使用情况
  • g1_survivor_space: Survivor区空间使用情况
  • g1_old_gen: Old老年代空间使用情况

非堆空间信息

  • codeheap_'non-nmethods': 非方法代码堆大小
  • metaspace: 元数据空间使用情况
  • codeheap_'profiled_nmethods':
  • compressed_class_space: 压缩类空间使用情况

GC垃圾收集信息

  • gc.g1_young_generation.count:新生代gc的数量
  • gc.g1_young_generation.time(ms)新生代gc的耗时
  • gc.g1_old_generation.count: 老年代gc的数量
  • gc.g1_old_generation.time(ms):老年代gc的耗时
  • os.name:当前使用的操作系统 Mac OS X
  • os.version :操作系统的版本号 10.16
  • java.version:java版本号 11.0.2
  • java.home:java根目录 /Library/Java/JavaVirtualMachines/jdk-11.0.2.jdk/Contents/Home
  • systemload.average:系统cpu负载平均值4.43

​ load average值的含义

​ > 单核处理器

​ 假设我们的系统是单CPU单内核的,把它比喻成是一条单向马路,把CPU任务比作汽车。当车不多的时候,load <1;当车占满整个 马路的时候 load=1;当马路都站满了,而且马路外还堆满了汽车的时候,load>1

Load < 1

Load = 1
Load >1

​ > 多核处理器

​ 我们经常会发现服务器Load > 1但是运行仍然不错,那是因为服务器是多核处理器(Multi-core)。

​ 假设我们服务器CPU是2核,那么将意味我们拥有2条马路,我们的Load = 2时,所有马路都跑满车辆。

Load = 2时马路都跑满了

  • processors : 处理器个数 8
  • timestamp/uptime:采集的时间戳Fri Jan 07 11:36:12 CST 2022/2349s

通过仪表盘,我们能从整体了解当前线程的运行健康状况

通过dashboard我们可以看到当前进程下运行的所有的线程。那么如果想要具体查看某一个线程的运行情况,可以使用thread命令

先来看看常用的参数。

我们的目标是想要找出CPU使用率最高的n个线程。那么需要先明确,如何计算出CPU使用率,然后才能找到最高的。计算规则如下:

 
  

统计1秒内cpu使用率最高的n个线程:

 
  

从线程的详情可以分析出,目前第一个线程的使用率是最高的,cpu占用了达到99.38%。第二行告诉我们,是Arthas.java这个类的第38行导致的。

由此,我们可以一眼看出问题,然后定位问题代码的位置,接下来就是人工排查问题了。

命令:

 
  

可以看到内容提示,线程Thread-1被线程Thread-0阻塞。对应的代码行数是DeadLockTest.java类的第31行。根据这个提示去查找代码问题。

命令

 
  

这个的含义是个1s统计一次采样

说道Arthas,不得不提的一个功能就是线上反编译代码的功能。经常会发生的一种状况是,线上有问题,定位问题后立刻改代码,可是发版后发现没生效,不可能啊~~~刚刚提交成功了呀。于是重新发版,只能靠运气,不知道为啥没生效。

反编译线上代码可以让我们一目了然知道代码带动部分是否生效。反编译代码使用Arthas的jad命令

 
  

用法:

 
  

运行结果:

运行结果分析:这里包含3个部分

  • ClassLoader:类加载器就是加载当前类的是哪一个类加载器
  • Location: 类在本地保存的位置
  • 源码:类反编译字节码后的源码

如果不想想是类加载信息和本地位置,只想要查看类源码信息,可以增加--source-only参数

 
  

能够调用线上的代码,是不是很神奇了。感觉哪段代码执行有问题,但是又没有日志,就可以使用这个方法动态调用目标方法了。

我们下面的案例都是基于这段代码执行,User类:

 
  

DeadLockTest类:

 
  

> 返回值是字符串

 
  

示例1:在DeadLockTest类中有一个add静态方法,我们来看看通过ognl怎么执行这个静态方法。执行命令

 
  

运行效果:

我们看到了这个对象的返回值是

> 返回值是对象

 
  

这里我们可以尝试一下替换-x 2 为 -x 1 ;-x 3;

* 案例1:返回对象的地址。不加 -x 或者是-x 1
 
  

返回值

* 案例2:返回对象中具体参数的值。加 -x 2
 
  

返回值

* 案例3:返回对象中有其他对象
  • 命令:
 
  

执行结果:

-x 2 获取的是对象的值,List返回的是数组信息,数组长度。

  • 命令:
 
  

执行结果:

-x 3 打印出对象的值,对象中List列表中的值。

* 案例4:方法A的返回值当做方法B的入参
 
  

> 方法入参是简单类型的列表

 
  

> 方法入参是一个复杂对象

 
  

> 方法入参是一个map对象

 
  

 
  

示例:在DeadLockTest类中有一个names静态属性,下面来看看如何获取这个静态属性。执行命令:

 
  

运行效果:

第一次执行获取属性命令,返回的属性是一个空集合;然后执行add方法,往names集合中添加了属性;再次请求names集合,发现有4个属性返回。

 
  

获取实例对象,使用new关键字,执行结果:

生产环境有时会遇到非常紧急的问题,或突然发现一个bug,这时候不方便重新发版,或者发版未生效,可以使用Arthas临时修改线上代码。通过Arthas修改的步骤如下:

 
  
 
  

使用sc命令查看JVM已加载的类信息。关于sc命令,查看官方文档:https://arthas.aliyun.com/doc/sc.html

  • -d : 表示打印类的详细信息

最后一个参数classLoaderHash,表示在jvm中类加载的hash值,我们要获得的就是这个值。

 
  
  • jad命令是反编译指定已加载类的源码
  • -c : 类所属 ClassLoader 的 hashcode
  • --source-only:默认情况下,反编译结果里会带有信息,通过选项,可以只打印源代码。
  • com.lxl.jvm.DeadLockTest:目标类的全路径
  • /Users/lxl/Downloads/DeadLockTest.java:反编译文件的保存路径
 
  

这里截取了部分代码。

 
  
 
  
  • mc: 编译.java文件生.class文件, 详细使用方法参考官方文档https://arthas.aliyun.com/doc/mc.html
  • -c:指定classloader的hash值
  • -d:指定输出目录
  • 最后一个参数是java文件路径

这是反编译后的class字节码文件

 
  

最后看到redefine success,表示重新加载.class文件进JVM成功了。

redefine命令使用之后,再使用jad命令会使字节码重置,恢复为未修改之前的样子。官方关于redefine命令的说明

这里检测效果,调用接口,执行日志即可。

这个功能也很好用,通常,我们在日志中打印的日志级别一般是infor、warn、error级别的,debug日志一般看不到。那么出问题的时候,一些日志,在写代码的时候会被记录在debug日志中,而此时日志级别又很高。那么迫切需要调整日志级别。

这个功能很好用啊,我们可以将平时不经常打印出来的日志设置为debug级别。设置线上日志打印级别为info。当线上有问题的时候,可以将日志级别动态调整为debug。异常排查完,在修改回info。这对访问量特别大日志内容很多的项目比较有效,可以有效节省日志输出带来的开销。

  • 当前应用的日志级别是info
  • 类加载的hash值是18b4aac2

我们定义一个接口,其源代码内容如下:

 
  

可以调用接口,查看日志输出代码。

我们看到,日志输出的是info及以下的级别。

 
  

修改完日志级别以后,输出日志为debug级别。

通常查询jvm参数,使用的是Java自带的工具[jinfo 进程号]。arthas中通过vmoption获取jvm参数:

假设,我们要设置JVM出现OutOfMemoryError的时候,自动dump堆快照

 
  

这时,如果发生堆内存溢出,会打印日志到文件

 
  
 
  

  • 通过圈起来的部分可以看到,接口的入口函数time总耗时371ms
  • 其中getDataFromDb函数耗时200ms
  • getDataFromRedis函数耗时100ms
  • getDataFromOuter函数耗时50ms
  • process函数耗时20ms

很明显,最慢的函数已经找到了,接下里就要去对代码进行进一步分析,然后再进行优化

到此这篇jvisualvm分析dump文件 定位大对象(java visualvm分析 dump)的文章就介绍到这了,更多相关内容请继续浏览下面的相关推荐文章,希望大家都能在编程的领域有一番成就!

版权声明


相关文章:

  • Java字符串转为数字(java 字符串转为数字)2025-11-08 22:27:05
  • java spring实战(javabean spring)2025-11-08 22:27:05
  • onnx模型部署(onnx模型部署java)2025-11-08 22:27:05
  • java面试题库及答案(java面试题和答案)2025-11-08 22:27:05
  • java date工具类(java date 类)2025-11-08 22:27:05
  • Java中字符串转int(JAVA中字符串转成set方法)2025-11-08 22:27:05
  • java面试题八股文面试(java八股文是什么意思)2025-11-08 22:27:05
  • Json字符串转实体类(json字符串转json对象java)2025-11-08 22:27:05
  • 学java的平台(学java软件)2025-11-08 22:27:05
  • map转json(map转json java)2025-11-08 22:27:05
  • 全屏图片