虚拟机性能监控与故障处理

虚拟机性能监控与故障处理

1.基础故障处理工具

1.1jps:虚拟机进程状况工具

JDK的很多小工具的名字都参考了UNIX命令的命名方式,jps(JVM Process Status Tool)是其中的典型。除了名字像UNIX的ps命令之外,它的功能也和ps命令类似:可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)名称以及这些进程的本地虚拟机唯一ID(LVMID,Local Virtual Machine Identifier)。虽然功能比较单一,但它绝对是使用频率最高的JDK命令行工具,因为其他的JDK工具大多需要输入它查询到的LVMID来确定要监控的是哪一个虚拟机进程。对于本地虚拟机进程来说,LVMID与操作系统的进程ID(PID,Process Identifier)是一致的,使用Windows的任务管理器或者UNIX的ps命令也可以查询到虚拟机进程的LVMID,但如果同时启动了多个虚拟机进程,无法根据进程名称定位时,那就必须依赖jps命令显示主类的功能才能区分了。

image-20240416141030255

测试jps命令的使用样例:

1
2
3
4
5
6
public class ScannerTest {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
scanner.nextLine();
}
}

image-20240416141506810

image-20240416141559993

image-20240416141113277

1.2jstat:虚拟机统计信息监控工具

jstat(JVM Statistics Monitoring Tool)是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类加载内存垃圾收集即时编译等运行时数据,在没有GUI图形界面、只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的常用工具。

image-20240416195203014

测试jstat命令的使用样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* VM:-Xms60m -Xmx60m -XX:survivorRatio=8
*/
public class JstatTest {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
try {
// 持续创建100KB的数组对象
list.add(new byte[1024 * 100]);
Thread.sleep(100);
} catch (Exception e) {

}
}
}
}

选项option代表用户希望查询的虚拟机信息,主要分为三类:类加载垃圾收集运行期编译状况

image-20240416195953566

类加载相关信息

image-20240416203516453

垃圾收集相关信息

image-20240416203425766

运行期编译状况相关信息

image-20240416203621947

使用jstat工具在纯文本状态下监视虚拟机状态的变化,在用户体验上也许不如后文将会提到的JMCVisualVM等可视化的监视工具直接以图表展现那样直观,但在实际生产环境中不一定可以使用图形界面,而且多数服务器管理员也都已经习惯了在文本控制台工作,直接在控制台中使用jstat命令依然是一种常用的监控方式。

1.3jinfo:Java配置信息工具

jinfo(Configuration Info for Java)的作用是实时查看和调整虚拟机各项参数。使用jps命令的-v参数可以查看虚拟机启动时显式指定的参数列表,但如果想知道未被显式指定的参数的系统默认值,除了去找资料外,就只能使用jinfo的-flag选项进行查询了(如果只限于JDK 6或以上版本的话,使用-XX:+PrintFlagsFinal查看参数默认值也是一个很好的选择)。

image-20240416204315548

执行样例:查询CMSInitiatingOccupancyFraction参数值:

1
2
3
4
5
6
7
8
/**
* VM:-Xms100m -Xmx100m
*/
public class JinfoTest {
public static void main(String[] args) throws InterruptedException {
new CountDownLatch(1).await();
}
}

image-20240416205751736

1.4jmap:Java内存映像工具

jmap(Memory Map for Java)命令用于生成堆转储快照(一般称为heapdump或dump文件)。如果不使用jmap命令,要想获取Java堆转储快照也还有一些比较“暴力”的手段:譬如-XX:+HeapDumpOnOutOfMemoryError参数,可以让虚拟机在内存溢出异常出现之后自动生成堆转储快照文件,通过-XX:+HeapDumpOnCtrlBreak参数则可以使用[Ctrl]+[Break]键让虚拟机生成堆转储快照文件,又或者在Linux系统下通过Kill-3命令发送进程退出信号“恐吓”一下虚拟机,也能顺利拿到堆转储快照。

jmap的作用并不仅仅是为了获取堆转储快照,它还可以查询finalize执行队列、Java堆和方法区的详细信息,如空间使用率、当前用的是哪种收集器等。

image-20240416211850265

执行样例:手动利用jmap -dump:live,format=b,file=heap.bin <pid>导出堆转储文件的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* VM:-Xms60m -Xmx60m -XX:SurvivorRatio=8
*/
public class JmapTest {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
try {
// 持续创建100KB的数组对象
list.add(new byte[1024 * 100]);
Thread.sleep(100);
} catch (Exception e) {

}
}
}
}

image-20240416220459118

执行样例:利用-XX:+HeapDumpOnOutofMemoryError -XX:HeapDumpPath:C:\Users\34404\oom.hprof自动导出堆转储文件的方式:

image-20240416221145093

image-20240416221308097

image-20240416221344047

执行样例:利用jmap -histo <pid>显示堆中对象统计信息:

image-20240416220711589

由于jmap需要访问堆中的所有对象,为了保证在此过程中不被应用线程干扰,jmap需要借助安全点机制,让所有的线程停留在不改变堆中对象引用的状态。也就是说,由jmap导出的堆转储文件必定是安全点位置的,这可能导致基于该dump文件的分析结果存在偏差,例如假设在编译生成的机器码中,某些对象的生命周期在两个安全点之间,那么:live选型将无法探知到这些对象。

另外,如果某个线程长时间无法跑到安全点,jmap会一直等下去,与jmap不同,垃圾收集器会主动将jstat所需要的统计信息保存在固定位置供jstat直接读取。

1.5jhat:虚拟机堆转储快照分析工具

JDK提供jhat(JVM Heap Analysis Tool)命令与jmap搭配使用,来分析jmap生成的堆转储快照。jhat内置了一个微型的HTTP/Web服务器,生成堆转储快照的分析结果后,可以在浏览器中查看。不过实事求是地说,在实际工作中,除非手上真的没有别的工具可用,否则多数人是不会直接使用jhat命令来分析堆转储快照文件的,主要原因有两个方面。

  • 一是一般不会在部署应用程序的服务器上直接分析堆转储快照,即使可以这样做,也会尽量将堆转储快照文件复制到其他机器上进行分析,因为分析工作是一个耗时而且极为耗费硬件资源的过程,既然都要在其他机器上进行,就没有必要再受命令行工具的限制了。
  • 另外一个原因是jhat的分析功能相对来说比较简陋,后文将会介绍到的VisualVM,以及专业用于分析堆转储快照文件的Eclipse Memory Analyzer等工具,都能实现比jhat更强大专业的分析功能。

分析结果默认以包为单位进行分组显示,分析内存泄漏问题主要会使用到其中的“Heap Histogram”(与jmap -histo功能一样)与OQL页签的功能,前者可以找到内存中总容量最大的对象,后者是标准的对象查询语言,使用类似SQL的语法对内存中的对象进行查询统计。

image-20240416224131763

1.6jstack:Java堆栈跟踪工具

jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等,都是导致线程长时间停顿的常见原因。线程出现停顿时通过jstack来查看各个线程的调用堆栈,就可以获知没有响应的线程到底在后台做些什么事情,或者等待着什么资源。

image-20240416224415720

执行样例:利用jstack命令检查线程间死锁问题:

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
public class JstackTest {
private static Object o1 = new Object();
private static Object o2 = new Object();

public static void main(String[] args) {
Thread threadA = new Thread(() -> {
synchronized (o1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {

}
synchronized (o2) {
System.out.println("执行成功" + Thread.currentThread().getName());
}
}
}, "threadA");
Thread threadB = new Thread(() -> {
synchronized (o2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {

}
synchronized (o1) {
System.out.println("执行成功" + Thread.currentThread().getName());
}
}
}, "threadB");
threadA.start();
threadB.start();
}
}

image-20240416225417623

image-20240416225440944

从JDK 5起,java.lang.Thread类新增了一个getAllStackTraces()方法用于获取虚拟机中所有线程的StackTraceElement对象。使用这个方法可以通过简单的几行代码完成jstack的大部分功能,在实际项目中不妨调用这个方法做个管理员页面,可以随时使用浏览器来查看线程堆栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class JavaAPITest {
public static void main(String[] args) {
for (Map.Entry<Thread, StackTraceElement[]> stackTrace : Thread.getAllStackTraces().entrySet()) {
Thread thread = (Thread) stackTrace.getKey();
StackTraceElement[] stack = (StackTraceElement[]) stackTrace.getValue();
if (thread.equals(Thread.currentThread())) {
continue;
}
System.out.print("\n线程:" + thread.getName() + "\n");
for (StackTraceElement element : stack) {
System.out.print("\t"+element+"\n");
}
}
}
}

image-20240416230513693

2.可视化故障处理工具

2.1jconsole:Java监视与管理控制台

JConsole(Java Monitoring and Management Console)是一款基于JMX(Java Management Extensions)的可视化监视、管理工具。它的主要功能是通过JMX的MBean(Managed Bean)对系统进行信息收集和参数动态调整。JMX是一种开放性的技术,不仅可以用在虚拟机本身的管理上,还可以运行于虚拟机之上的软件中,典型的如中间件大多也基于JMX来实现管理与监控。虚拟机对JMX MBean的访问也是完全开放的,可以使用代码调用API、支持JMX协议的管理控制台,或者其他符合JMX规范的软件进行访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* VM:-Xms600m -Xmx600m -XX:SurvivorRatio=8
*/
public class HeapInstanceTest {
byte[] buffer = new byte[new Random().nextInt(1024 * 100)];

public static void main(String[] args) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ArrayList<HeapInstanceTest> list = new ArrayList<HeapInstanceTest>();
while (true) {
list.add(new HeapInstanceTest());
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

image-20240417092612902

image-20240417093314454

image-20240417093600283

image-20240417093641475

image-20240417093919094

image-20240417094232329

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
public class ThreadDeadLockTest {
private static Object o1 = new Object();
private static Object o2 = new Object();

public static void main(String[] args) {
Thread threadA = new Thread(() -> {
synchronized (o1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {

}
synchronized (o2) {
System.out.println("执行成功" + Thread.currentThread().getName());
}
}
}, "threadA");
Thread threadB = new Thread(() -> {
synchronized (o2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {

}
synchronized (o1) {
System.out.println("执行成功" + Thread.currentThread().getName());
}
}
}, "threadB");
threadA.start();
threadB.start();
}
}

image-20240417102419867

2.2VisualVM:多合一故障处理工具

VisualVM(All-in-One Java Troubleshooting Tool)是功能最强大的运行监视和故障处理程序之一,曾经在很长一段时间内是Oracle官方主力发展的虚拟机故障处理工具。Oracle曾在VisualVM的软件说明中写上了“All-in-One”的字样,预示着它除了常规的运行监视、故障处理外,还将提供其他方面的能力,譬如性能分析(Profiling)。VisualVM的性能分析功能比起JProfiler、YourKit等专业且收费的Profiling工具都不遑多让。而且相比这些第三方工具,VisualVM还有一个很大的优点:不需要被监视的程序基于特殊Agent去运行,因此它的通用性很强,对应用程序实际性能的影响也较小,使得它可以直接应用在生产环境中。这个优点是JProfiler、YourKit等工具无法与之媲美的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* VM:-Xms600m -Xmx600m -XX:SurvivorRatio=8
*/
public class HeapInstanceTest {
byte[] buffer = new byte[new Random().nextInt(1024 * 100)];

public static void main(String[] args) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ArrayList<HeapInstanceTest> list = new ArrayList<HeapInstanceTest>();
while (true) {
list.add(new HeapInstanceTest());
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

image-20240417102609513

image-20240417102829166

image-20240417103038979

image-20240417103442116

image-20240417103140681

2.3Memory Analyzer Tool:堆转储文件分析工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* -Xms600m -Xmx600m -XX:SurvivorRatio=8
*/
public class OOMTest {
public static void main(String[] args) {
ArrayList<Picture> list = new ArrayList<>();
while(true){
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(new Picture(new Random().nextInt(100 * 50)));
}
}
}

class Picture{
private byte[] pixels;

public Picture(int length) {
this.pixels = new byte[length];
}
}

image-20240418140057872

image-20240418140321919

image-20240418140554373

image-20240418141228415

image-20240418140846054

image-20240418141459276

image-20240418141536265

image-20240418141707685

3.JVM调优案例分析与实战

3.1内存泄漏问题分析与排查

Java中内存泄漏的8种常见情况:

  • 静态集合类:如HashMap、ArrayList等,如果这些容器为静态的,那么它们的生命周期与JVM程序一致,则容器中的对象在程序结束前不能被释放,从而造成内存泄漏。简而言之,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。
1
2
3
4
5
6
7
8
public class MemoryLeak {
static List list = new ArrayList();

public void oomTests() {
Object obj = new Object();
list.add(obj);
}
}
  • 单例模式:与静态集合导致内存泄漏的原因类似,因为单例的静态特性,它的生命周期与JVM的生命周期一样长,所以如果单例对象持有外部对象的引用,那么这个外部对象也不会被回收,从而造成内存泄漏。
  • 内部类持有外部类:如果一个外部类的实例对象的方法返回了一个内部类的实例对象,并且该内部类对象被长期引用,那么即使外部类对象不再使用,但由于内部类持有外部类的实例对象,这个外部类对象也不会被回收,从而造成内存泄漏。
  • 连接对象如数据库连接、网络连接、IO连接等:在对数据库进行操作时,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放数据库连接,否则如果不显式关闭Connection、Statement或ResultSet等,将会造成大量的对象无法被回收,从而造成内存泄漏。
  • 变量不合理的作用域:一般而言,一个变量定义的作用域大于其使用范围,很有可能会造成内存泄漏,另一方面,如果没有及时把对象设置为null,也很有可能导致内存泄漏的发生。
1
2
3
4
5
6
7
8
9
public class MemoryLeak {
private String msg;
public void receiveMsg() {
// 从网络接受数据保存到msg
readFromNetwork();
// 将msg保存到数据库中
saveDB();
}
}

上面的代码中通过readFromNetwork方法把接受的数据保存到变量msg中,然后调用saveDB方法把msg的内容保存到数据库中,此时msg已经没用了,但是由于msg的生命周期与对象的生命周期相同,此时msg还不能回收,因此导致内存泄漏。实际上,这个msg变量可以定义在readFromNetwork方法内部,当方法使用完毕,msg的生命周期也结束了,此时就可以回收了。还有一种方法,在使用完msg后,把msg置为null,这样也能被回收。

  • 改变哈希值:当一个对象放入HashSet中以后,就不能修改这个对象的参与哈希值计算的字段了,否则后续无法找到,导致无法删除,从而造成内存泄漏。
  • 缓存泄漏:可以使用WeakHashMap表示缓存,当没有其他外部强引用时,GC发生时可以回收掉缓存数据。
  • 监视器和回调:如果客户端在你实现的API中注册回调,却没有显式取消,那么就会累积。

内存泄漏的案例:

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
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;

public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}

public void push(Object e) { //入栈
ensureCapacity();
elements[size++] = e;
}

//存在内存泄漏
// public Object pop() { //出栈
// if (size == 0)
// throw new EmptyStackException();
// return elements[--size];
// }

public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}

private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}