【Java VisualVM】使用 VisualVM 进行性能分析及调优

转载:http://blog.csdn.net/lmb55/article/details/79267277

一、概述

开发大型 Java 应用程序的过程中难免遇到内存泄露、性能瓶颈等问题,比如文件、网络、数据库的连接未释放,未优化的算法等。随着应用程序的持续运行,可能会造成整个系统运行效率下降,严重的则会造成系统崩溃。为了找出程序中隐藏的这些问题,在项目开发后期往往会使用性能分析工具来对应用程序的性能进行分析和优化。

VisualVM 是一款免费的性能分析工具。它通过 jvmstat、JMX、SA(Serviceability Agent)以及 Attach API 等多种方式从程序运行时获得实时数据,从而进行动态的性能分析。同时,它能自动选择更快更轻量级的技术尽量减少性能分析对应用程序造成的影响,提高性能分析的精度。

本文将对 VisualVM 的主要功能逐一介绍并探讨如何利用获得的数据进行性能分析及调优。IBM开发文档

二、背景知识:性能分析的主要方式

1.监视:监视是一种用来查看应用程序运行时行为的一般方法。通常会有多个视图(View)分别实时地显示 CPU 使用情况、内存使用情况、线程状态以及其他一些有用的信息,以便用户能很快地发现问题的关键所在。

2.转储:性能分析工具从内存中获得当前状态数据并存储到文件用于静态的性能分析。Java 程序是通过在启动 Java 程序时添加适当的条件参数来触发转储操作的。它包括以下三种:

系统转储:JVM 生成的本地系统的转储,又称作核心转储。一般的,系统转储数据量大,需要平台相关的工具去分析,如 Windows 上的 windbg 和 Linux 上的 gdb.

Java 转储:JVM 内部生成的格式化后的数据,包括线程信息,类的加载信息以及堆的统计数据。通常也用于检测死锁。

堆转储:JVM 将所有对象的堆内容存储到文件。

3.快照:应用程序启动后,性能分析工具开始收集各种运行时数据,其中一些数据直接显示在监视视图中,而另外大部分数据被保存在内部,直到用户要求获取快照,基于这些保存的数据的统计信息才被显示出来。快照包含了应用程序在一段时间内的执行信息,通常有 CPU 快照和内存快照两种类型。

CPU 快照:主要包含了应用程序中函数的调用关系及运行时间,这些信息通常可以在 CPU 快照视图中进行查看。

内存快照:主要包含了内存的分配和使用情况、载入的所有类、存在的对象信息及对象间的引用关系等。这些信息通常可以在内存快照视图中进行查看。

4.性能分析:性能分析是通过收集程序运行时的执行数据来帮助开发人员定位程序需要被优化的部分,从而提高程序的运行速度或是内存使用效率,主要有以下三个方面:

CPU 性能分析:CPU 性能分析的主要目的是统计函数的调用情况及执行时间,或者更简单的情况就是统计应用程序的 CPU 使用情况。通常有 CPU 监视和 CPU 快照两种方式来显示 CPU 性能分析结果。

内存性能分析:内存性能分析的主要目的是通过统计内存使用情况检测可能存在的内存泄露问题及确定优化内存使用的方向。通常有内存监视和内存快照两种方式来显示内存性能分析结果。

线程性能分析:线程性能分析主要用于在多线程应用程序中确定内存的问题所在。一般包括线程的状态变化情况,死锁情况和某个线程在线程生命期内状态的分布情况等

三、VisualVM 安装

1、VisualVM 安装

VisualVM 是一个性能分析工具,自从 JDK 6 Update 7 以后已经作为 Oracle JDK 的一部分,位于 JDK 根目录的 bin 文件夹下。VisualVM 自身要在 JDK6 以上的版本上运行,但是它能够监控 JDK1.4 以上版本的应用程序。

VisualVM可以使用JDK自带的jvisualvm(bin目录下)也可以单独下载,经过实验,发现安装后的JDK中自带的jvisualvm包含的插件比较少大概有五六个左右,单独下载的插件包含的比较多大概有24个左右。它也可以监控1.6以前的JDK,但是对某些模块支持的并不是很好,无法显示。

2、安装 VisualVM 上的插件

VisualVM 插件中心提供很多插件以供安装向 VisualVM 添加功能。可以通过 VisualVM 应用程序安装,或者从 VisualVM 插件中心手动下载插件,然后离线安装。另外,用户还可以通过下载插件分发文件 (.nbm 文件 ) 安装第三方插件为 VisualVM 添加功能。

从 VisualVM 插件中心安装插件安装步骤 :

1、从主菜单中选择“工具”>“插件”。 
2、在“可用插件”标签中,选中该插件的“安装”复选框。单击“安装”。 
3、逐步完成插件安装程序

这里写图片描述

根据 .nbm 文件安装第三方插件安装步骤 :

1、从主菜单中选择“工具”>“插件”。 
2、在“已下载”标签中,点击”添加插件”按钮,选择已下载的插件分发文件 (.nbm) 并打开。 
3、选中打开的插件分发文件,并单击”安装”按钮,逐步完成插件安装程序

这里写图片描述

四、功能介绍

下面我们将介绍性能分析的几种常见方式以及如何使用 VisualVM 性能分析工具进行分析。

1、内存分析 
VisualVM 通过检测 JVM 中加载的类和对象信息等帮助我们分析内存使用情况,我们可以通过 VisualVM 的监视标签和 Profiler 标签对应用程序进行内存分析。 
在监视标签内,我们可以看到实时的应用程序内存堆以及永久保留区域的使用情况。 
内存堆使用情况 
这里写图片描述

永久保留区域使用情况 
这里写图片描述

此外,我们也可以通过 Applications 窗口右击应用程序节点来启用“在出现 OOME 时生成堆 Dump”功能,当应用程序出现 OutOfMemory 例外时,VisualVM 将自动生成一个堆转储。 
开启“在出现 OOME 时生成堆”功能 
这里写图片描述 
在 Profiler 标签,点击“内存”按钮将启动一个内存分析会话,等 VisualVM 收集和统计完相关性能数据信息,将会显示在性能分析结果。通过内存性能分析结果,我们可以查看哪些对象占用了较多的内存,存活的时间比较长等,以便做进一步的优化。 
此外,我们可以通过性能分析结果下方的类名过滤器对分析结果进行过滤。 
内存分析结果 
这里写图片描述

2、CPU 分析 
VisualVM 能够监控应用程序在一段时间的 CPU 的使用情况,显示 CPU 的使用率、方法的执行效率和频率等相关数据帮助我们发现应用程序的性能瓶颈。我们可以通过 VisualVM 的监视标签和 Profiler 标签对应用程序进行 CPU 性能分析。

在监视标签内,我们可以查看 CPU 的使用率以及垃圾回收活动对性能的影响。过高的 CPU 使用率可能是由于我们的项目中存在低效的代码,可以通过 Profiler 标签的 CPU 性能分析功能进行详细的分析。如果垃圾回收活动过于频繁,占用了较高的 CPU 资源,可能是由内存不足或者是新生代和旧生代分配不合理导致的等。 
CPU 使用情况 
这里写图片描述 
在 Profiler 标签,点击“CPU”按钮启动一个 CPU 性能分析会话 ,VisualVM 会检测应用程序所有的被调用的方法。当进入一个方法时,线程会发出一个“method entry”的事件,当退出方法时同样会发出一个“method exit”的事件,这些事件都包含了时间戳。然后 VisualVM 会把每个被调用方法的总的执行时间和调用的次数按照运行时长展示出来。

此外,我们也可以通过性能分析结果下方的方法名过滤器对分析结果进行过滤。 
CPU 性能分析结果 
这里写图片描述

3、线程分析 
Java 语言能够很好的实现多线程应用程序。当我们对一个多线程应用程序进行调试或者开发后期做性能调优的时候,往往需要了解当前程序中所有线程的运行状态,是否有死锁、热锁等情况的发生,从而分析系统可能存在的问题。

在 VisualVM 的监视标签内,我们可以查看当前应用程序中所有活动线程和守护线程的数量等实时信息。 
活跃线程情况 
这里写图片描述 
VisualVM 的线程标签提供了三种视图,默认会以时间线的方式展现。另外两种视图分别是表视图和详细信息视图。

时间线视图上方的工具栏提供了缩小,放大和自适应三个按钮,以及一个下拉框,我们可以选择将所有线程、活动线程或者完成的线程显示在视图中。 
线程时间线视图 
这里写图片描述

线程表视图 
这里写图片描述 
我们在详细信息视图中不但可以查看所有线程、活动线程和结束的线程的详细数据,而且也可以查看某个线程的详细情况。 
线程详细视图 
这里写图片描述

五、快照功能

我们可以使用 VisualVM 的快照功能生成任意个性能分析快照并保存到本地来辅助我们进行性能分析。快照为捕获应用程序性能分析数据提供了一个很便捷的方式因为快照一旦生成可以在任何时候离线打开和查看,也可以相互传阅。

VisualVM 提供了两种类型的快照: 
1、Profiler 快照:当有一个性能分析会话(内存或者 CPU)正在进行时,我们可以通过性能分析结果工具栏的“快照”按钮生成 Profiler 快照捕获当时的性能分析数据。 
Profiler 快照 
这里写图片描述

2、应用程序快照:我们可以右键点击左侧 Applications 窗口中应用程序节点,选择“应用程序快照”为生成一个应用程序快照。应用程序快照会收集某一时刻的堆转储,线程转储和 Profiler 快照,同时也会捕获 JVM 的一些基本信息。 
应用程序快照 
这里写图片描述

六、转储功能

1、线程转储的生成与分析

VisualVM 能够对正在运行的本地应用程序生成线程转储,把活动线程的堆栈踪迹打印出来,帮助我们有效了解线程运行的情况,诊断死锁、应用程序瘫痪等问题。 
线程标签及线程转储功能 
这里写图片描述

当 VisualVM 统计完应用程序内线程的相关数据,会把这些信息显示新的线程转储标签。 
线程转储结果 
这里写图片描述

2、堆转储的生成与分析

VisualVM 能够生成堆转储,统计某一特定时刻 JVM 中的对象信息,帮助我们分析对象的引用关系、是否有内存泄漏情况的发生等。 
监视标签及堆转储功能 
这里写图片描述

当 VisualVM 统计完堆内对象数据后,会把堆转储信息显示在新的堆转储标签内,我们可以看到摘要、类、实例数等信息以及通过 OQL 控制台执行查询语句功能。 
堆转储的摘要包括转储的文件大小、路径等基本信息,运行的系统环境信息,也可以显示所有的线程信息。 
堆转储的摘要视图 
这里写图片描述

从类视图可以获得各个类的实例数和占用堆大小数,分析出内存空间的使用情况,找出内存的瓶颈,避免内存的过度使用。 
堆转储的类视图 
这里写图片描述

通过实例数视图可以获得每个实例内部各成员变量的值以及该实例被引用的位置。首先需要在类视图选择需要查看实例的类。 
选择查询实例数的类 
这里写图片描述 
实例数视图 
这里写图片描述

此外,还能对两个堆转储文件进行比较。通过比较我们能够分析出两个时间点哪些对象被大量创建或销毁。 
堆转储的比较 
这里写图片描述 
堆转储的比较结果 
这里写图片描述 
线程转储和堆转储均可以另存成文件,以便进行离线分析。 
转储文件的导出 
这里写图片描述

七、实战分析

1、简要说明

打开jdk自带的jvisualvm(bin目录下),程序运行后会自动监控本机运行的java程序(Local标签下,远程服务器上的java程序需要另行配置),Local标签下的第一个VisualVM为jvisualvm对自身的监控,可以看到消耗的资源还是很少的,第二个为本机的eclipse。 
这里写图片描述

监控项总共分为Overview,Monitor,Threads和一个Sampler。 
(1)Overview(jvm启动参数,系统参数) 
这里写图片描述 
可以看到eclipse的启动参数 
这里写图片描述 
(通过这些启动参数,可以判断程序是否有内存溢出)

(2)Monitor 
这里写图片描述 
左上:cpu利用率,gc状态的监控 
右上:堆利用率,永久内存区的利用率 
左下:类的监控 
右下:线程的监控 
performGC:gc的详细运行状态 
HeapDump:堆的详细状态(可以看到堆的概况,里面所有的类,还能点进具体的一个类查看这个类的状态)

(3)Threads 
这里写图片描述 
能够显示线程的名称和运行的状态,在调试多线程时必不可少,而且可以点进一个线程查看这个线程的详细运行情况

2、监控服务器上的tomcat

tomcat的配置文件catalina.sh中增加:

JAVA_OPTS="-Dcom.sun.management.jmxremote.port=9998   
    -Dcom.sun.management.jmxremote.ssl=false   
    -Dcom.sun.management.jmxremote.authenticate=false   
    -Djava.rmi.server.hostname=192.168.58.164" 

参数说明:
指定了JMX启动的代理端口,这个端口就是visualvm要连接的端口(9998端口不能被别的程序使用netstat -an|gerp 9998)  
Dcom.sun.management.jmxremote.port=9998 

指定了JMX是否启用ssl  
Dcom.sun.management.jmxremote.authenticate=false  

指定了JMX是否启用鉴权(需要用户名,密码鉴权)  
Dcom.sun.management.jmxremote.authenticate=false  

指定了服务器主机名  
Djava.rmi.server.hostname=192.168.58.164  

这里写图片描述 
填写主机名: 
这里写图片描述 
右键创建一个jmx连接,填写上ip:port即可: 
这里写图片描述

3、监控服务器上的java程序

相较于监控tomcat要麻烦很多,要预先启动jstatd服务(${java_home}/bin目录下) 
jstatd是一个监控JVM从创建到销毁过程中资源占用情况并提供远程监控接口的RMI(Remote Method Invocation,远程方法调用)服务器程序,它是一个Daemon程序(后台进程),要保证远程监控软件连接到本地的话需要jstatd始终保持运行。

jstatd运行需要通过-J-Djava.security.policy=*指定安全策略,因此我们需要在服务器上建立一个指定安全策略的文件jstatd.all.policy(我放在了${java_home}/bin目录下),文件内容如下:

grant codebase "file:/home/123/123/jdk1.5.0_15/lib/tools.jar" {   
    permission java.security.AllPermission;   
};   

然后使用这个策略文件启动jstatd服务

[sys@sys bin]$ pwd  
/home/sys/jdk1.5.0_15/bin  
[sys@123 sys]$ ./jstatd -J-Djava.security.policy=./jstatd.all.policy & 

因为监控的过程中需要jstatd服务一直运行,所以加上了&,如果需要日志也可使用:

./jstatd -J-Djava.security.policy=./jstatd.all.policy -J-Djava.rmi.server.logCalls=true  

接下来就可以在jvisualvm中配置监控该服务器上运行的java程序了,和在jvisualvm中配置监控tomcat服务器的操作过程是一样的。需要特别注意的是,有时在配置远程监控java程序的时候jvisualvm会报一个错误 
点击查看错误详情: 
这里写图片描述 
connection refused to host:127.0.0.1初步判断和主机名有关系。

[sys@sys bin]# hostname -i  
127.0.0.1  

[sys@sys bin]# hostname 192.168.58.168 

修改完重启jstatd服务(网上很多人说要修改主机的/etc/hosts文件,但是我自己测试修改/etc/hosts文件是没有效果的,必须要修改主机名),Add Rempte Host,填写主机名,之后这里要选择添加一个jstatd连接: 
这里写图片描述 
直接选择默认配置即可(默认使用1099端口): 
这里写图片描述 
点击ok后,168上的所有java程序就会自动列出: 
这里写图片描述

注意:推荐一个非常好用的插件VisualGC,tool -> plugin ->aviable plugin: 
这里写图片描述 
安装完这个插件后,将会增加新的监控条目Visual GC,可以看到虚拟机内存各个区的使用情况: 
这里写图片描述

4、模拟内存泄漏

import java.util.HashMap;  
import java.util.Map;  
public class MemoryLeckTest {  
    //声明缓存对象  
    private static final Map map = new HashMap();  
    public static void main(String args[]){  
        try {  
            Thread.sleep(10000);//给打开visualvm时间  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        //循环添加对象到缓存  
        for(int i=0; i<1000000;i++){  
            TestMemory t = new TestMemory();  
            map.put("key"+i,t);  
        }  
        System.out.println("first");  
        //为dump出堆提供时间  
        try {  
            Thread.sleep(10000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        for(int i=0; i<1000000;i++){  
            TestMemory t = new TestMemory();  
            map.put("key"+i,t);  
        }  
        System.out.println("second");  
        try {  
            Thread.sleep(10000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        for(int i=0; i<3000000;i++){  
            TestMemory t = new TestMemory();  
            map.put("key"+i,t);  
        }  
        System.out.println("third");  
        try {  
            Thread.sleep(10000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        for(int i=0; i<4000000;i++){  
            TestMemory t = new TestMemory();  
            map.put("key"+i,t);  
        }  
        System.out.println("forth");  
        try {  
            Thread.sleep(Integer.MAX_VALUE);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        System.out.println("qqqq");  
    }  
}  

配置的JVM参数如下:

-Xms512m  
-Xmx512m  
 -XX:-UseGCOverheadLimit  
 -XX:MaxPermSize=50m  

使用jVisualvm分析内存泄漏:

查看Visual GC标签,内容如下,这是输出first的截图 
这里写图片描述 
这是输出forth的截图: 
这里写图片描述 
通过2张图对比发现: 
这里写图片描述 
老生代一直在gc,当程序继续运行可以发现老生代gc还在继续: 
这里写图片描述 
增加到了7次,但是老生代的内存并没有减少。说明存在无法被回收的对象,可能是内存泄漏了。 
如何分析是那个对象泄漏了呢?打开抽样器标签:点击后如下图: 
这里写图片描述 
按照程序输出进行堆dump,当输出second时,dump一次,当输出forth时dump一次。 
进入最后dump出来的堆标签,点击类: 
这里写图片描述 
点击右上角:“与另一个堆存储对比”。如图选择第一次导出的dump内容比较: 
这里写图片描述 
比较结果如下: 
这里写图片描述 
可以看出在两次间隔时间内TestMemory对象实例一直在增加并且多了,说明该对象引用的方法可能存在内存泄漏。 
如何查看对象引用关系呢? 
右键选择类TestMemory,选择“在实例视图中显示”,如下所示: 
这里写图片描述 
左侧是创建的实例总数,右侧上部为该实例的结构,下面为引用说明,从图中可以看出在类CyclicDependencies里面被引用了,并且被HashMap引用。如此可以确定泄漏的位置,进而根据实际情况进行分析解决。

参考:http://blog.csdn.net/kl28978113/article/details/53817827

posted @ 2018-02-13 17:32  小学生II  阅读(1407)  评论(0编辑  收藏  举报