对性能瓶颈问题来源定位的可视化工具

  • OutOfMemoryError
  • 内存泄漏
  • 线程死锁
  • 锁争用(Lock Contention)
  • Java进程消耗CPU过高

在JDK6u7后,VisualVM作为Sun的JDK发行版的标准部分,可以替代例如jinfojmapjstackjstatjConsole

JConsole

介绍

版本:JDK1.5+

Java Monitoring and Management Console,基于JMX的可视化监视、管理工具。

JMX(Java Management Extensions,即管理Java的扩展),这种机制可以方便的管理正在运行中的Java程序。常用于管理线程、内存、日志级别、服务重启、系统环境等。一个JMX管理资源可以是一个Java应用、一个服务或一个设备,它们可以用Java开发,或者至少能用Java进行包装,并且能被置入JMX框架中,从而成为JMX的一个管理构件(Managed Bean),简称MBean。

使用

命令行中输入jconsole回车,将自动搜索出本机运行的所有进程,双击选择要监控的进程即可,也可以远程连接服务器。

  • 概述
    • 堆内存使用量、线程、类、CPU使用情况
    • 对着图右击可以保存数据到CVS文件
  • 内存
    • 可视化的jstat,用于监视受收集器管理的虚拟机内存的变化趋势
    • 查看不同内存(堆内存、非堆内存),使用的GC算法及回收次数和时间
    • 前提要学习好Java的内存模型
  • 线程
    • 可以输入字符串过滤关键线程
    • 查看某一线程的名称、状态、阻塞和等待的次数、堆栈的信息
    • 红色:线程数目的峰值;蓝色:当前活动的线程
    • 检测死锁(D),有时很有用
    • 红线:总共加载的类(包括后来卸载的)
    • 蓝线:当前的类加载
  • VM摘要
    • 看看线程、类、内存、系统、和其它信息
  • MBean

VisualVM

介绍

版本:JDK6u7+

不需要被监视的程序基于特殊Agent运行,对程序的实际性能影响很小,可以直接应用在生产环境中。

通过插件拓展的特性,可以做到:

  • 显示JVM进程和进程的配置、环境信息(jps、jinfo)
  • 监视应用程序的CPU、GC、堆、方法区及线程的信息(stat、jstack)
  • dump及分析堆转储快照(jmap、jhat)
  • 方法级的程序运行性能分析,找出被调用最多、运行时间最长的方法
  • 离线程序快照:收集程序的运行时配置、线程dump、内存dump等信息建立一个快照

使用

  • 安装
  • 生成堆dmp文件
    • Aplications窗口右击节点,选择Heap Dump
    • Monitor,点击Heap Dump
  • Profiler中可以对运行期间方法级的CPU和内存进行分析,但会影响程序运行性能,不建议在生产环境使用。
    • CPU:统计每个方法的执行次数、执行耗时
    • Memory:统计每个方法关联的对象数及对象所占空间

JDK1.5后,如果使用Client模式,在进行Profiler之前,最好在监视程序中加入-Xshare:off来关闭类共享优化。

Client模式下的虚拟机加入并自动开启了类共享,在多虚拟机进程中共享rt.jar中类数据以提高加载速度和节省内存的优化,但这可能会导致被监视的应用程序崩溃。

  • BTrace动态日志跟踪:在不停止程序运行的前提下,通过热部署技术加入原本并不存在的调试代码,以实现对程序的动态调试
    • 打印调用堆栈、参数、返回值
    • 性能监控、定位连接泄漏和内存泄漏、解决多线程竞争问题
  • Threads
    • 线程长时间停顿的原因
      • 等待外部资源(数据库连接、网络资源、设备资源等)
      • 死循环
      • 锁等待(活锁和死锁)

插件

  • 插件中心
    • 菜单 > Tools > Plugins > Available Plugins > Install
  • 下载插件分发文件(.nbm)
    • 菜单 > Tools > Plugins > Downloaded > Add Plugins > 选择.nbm文件 > Install

实例-填充内存

  • 写代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.ArrayList;
import java.util.List;

public class OOMObject {
private byte[] placeHolder=new byte[64*1024];

public static void fillHeap(int num) throws InterruptedException{
List<OOMObject> list=new ArrayList<>();
for(int i=0;i<num;i++){
Thread.sleep(50);
list.add(new OOMObject());
}
System.gc();
}

public static void main(String[] args)throws Exception{
fillHeap(1000);
}
}
  • 改JVM参数
1
-Xms100m -Xmx100m -XX:+UseSerialGC
  • 运行后可看到,内存Eden区呈折线状,整个堆的曲线是平滑向上增长。

实例-线程死循环&等待

  • 写代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import java.io.BufferedReader;
import java.io.InputStreamReader;

public class Lock {
/**
* 线程死循环
*/
public static void createBusyThread(){
Thread thread =new Thread(new Runnable() {

@Override
public void run() {
while(true);

}
},"testBusyThread");
thread.start();
}

/**
* 线程锁等待
* @param lock
*/
public static void createLockThread(final Object lock){
Thread thread =new Thread(new Runnable() {

@Override
public void run() {
synchronized (lock) {
try{
lock.wait();
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"testLockThread");
thread.start();
}

public static void main(String[] args) throws Exception{
BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(System.in));
bufferedReader.readLine();
createBusyThread();
bufferedReader.readLine();
Object object=new Object();
createLockThread(object);
}
}
  • 运行后,查看VisualVM中的Threads中的main线程,会发现一直在readBytes等待System.in的键盘输入,线程状态为Runnable,线程会被分配运行时间,但readBytes检查到流没有更新时会立刻归还执行令牌,这种等待只消耗很小的CPU资源。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
"main" - Thread t@1
java.lang.Thread.State: RUNNABLE
at java.io.FileInputStream.readBytes(Native Method)
at java.io.FileInputStream.read(FileInputStream.java:255)
at java.io.BufferedInputStream.read1(BufferedInputStream.java:284)
at java.io.BufferedInputStream.read(BufferedInputStream.java:345)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
at java.io.InputStreamReader.read(InputStreamReader.java:184)
at java.io.BufferedReader.fill(BufferedReader.java:161)
at java.io.BufferedReader.readLine(BufferedReader.java:324)
at java.io.BufferedReader.readLine(BufferedReader.java:389)
at com.super2bai.jvm.Lock.main(Lock.java:45)
  • 随便在控制台输入点内容回车后,刷新VisualVM中的Threads,会看到testBusyThread线程,因为一直在执行空循环,所以停留在Lock的15行,也就是while(true);,这时候线程状态为RUNNABLE,而且没有归还线程执行令牌的动作,会在空循环上用尽全部执行时间直到线程切换,这种等待会消耗较多的CPU资源。
1
2
3
4
"testBusyThread" - Thread t@17
java.lang.Thread.State: RUNNABLE
at com.super2bai.jvm.Lock$1.run(Lock.java:15)
at java.lang.Thread.run(Thread.java:748)
  • 继续在控制台输入内容后回车,刷新VisualVM中的Threads,会看到testLockThread线程,停留在Lock的33行,也就是lock.wait();,正在等待着lock对象的notify()notifyAll()方法的出现,线程处于WAITING状态,也就是正常的活锁等待,在被唤醒前不会被分配执行时间,只有lock对象的notify()notifyALL()方法被调用,便能激活以继续执行。
1
2
3
4
5
6
7
"testBusyThread" - Thread t@9
java.lang.Thread.State: WAITING
at java.lang.Object.wait(Native Method)
- waiting on <753b0264> (a java.lang.Object)
at java.lang.Object.wait(Object.java:502)
at com.super2bai.jvm.Lock$2.run(Lock.java:33)
at java.lang.Thread.run(Thread.java:748)

实例-线程死锁

  • 写代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class SynAddRunnable implements Runnable {

int a, b;

public SynAddRunnable(int a, int b) {
this.a = a;
this.b = b;
}

@Override
public void run() {
/**
* 死锁的原因:<br>
* Integer.valueOf()方法基于减少对象创建次数和节省内存的考虑<br>
* [-128, 127]之间的数字会被缓存<br>
* java.lang.Integer.IntegerCache中high和low的值<br>
* 当传入的值在这个范围内,会直接返回缓存中的对象<br>
* 也就是说,100次一共就只返回了两个不同的对象<br>
* 加入在某个线程的两个synchronized块之间发生了一次线程切换<br>
* 那就会出现线程A等待线程B持有的Integer.valueOf(1)<br>
* 而线程B又等待线程A持有的Integer.valueOf(2)<br>
* 就会发生死锁,互相等待,运行不下去了<br>
*/
synchronized (Integer.valueOf(a)) {
synchronized (Integer.valueOf(b)) {
System.out.println(a + b);
}
}
}

public static void main(String[] args) {
// 不用for其实也可以,但死锁概率小。
for (int i = 0; i < 100; i++) {
new Thread(new SynAddRunnable(1, 2)).start();
new Thread(new SynAddRunnable(2, 1)).start();
}
}
}
  • 运行后,查看JConsole内的线程,点击下方检测死锁按钮,会出现新的面板死锁,点开任意一个线程,即可看到两个线程互相等待。
1
2
3
4
5
6
7
名称: Thread-157
状态: java.lang.Integer@248beb75上的BLOCKED, 拥有者: Thread-158
总阻止数: 1, 总等待数: 0

堆栈跟踪:
com.super2bai.jvm.thread.SynAddRunnable.run(SynAddRunnable.java:28)
java.lang.Thread.run(Thread.java:748)

1
2
3
4
5
6
7
名称: Thread-158
状态: java.lang.Integer@47cf143上的BLOCKED, 拥有者: Thread-157
总阻止数: 1, 总等待数: 0

堆栈跟踪:
com.super2bai.jvm.thread.SynAddRunnable.run(SynAddRunnable.java:28)
java.lang.Thread.run(Thread.java:748)

Thread-157等待Thread-158持有的Integer对象,而Thread-158也在等待Thread-157持有的Integer对象,这样两个线程就互相卡住,都不存在等到锁释放的希望了。

对比

  • VisualVM功能稍强于Jconsole,VisualVM的高级功能是通过安装插件来拓展
  • VisualVM可以在Applications窗口右击程序节点启用“Disable Heap Dump on OOME”,VisualVM将自动生成一个堆转储文件

参考

JMX

IBM Java开发