使用SA分析内存溢出问题

背景

在阅读《Java性能调优指南》一书的最后,书中介绍了Serviceability Agent,并给出了一些排查问题的示例,我感觉看书不够深刻,因此自己在macOs上进行了一些实验。我的操作系统版本是:macOS Sierra 10.12.6,我的JDK版本是1.8.0_152。

例子程序

在Java开发中,常常遇到的一种问题是内存空间会越来越大,极端情况下会出现OOM——java.lang.OutOfMemoryError。产生内存不足错误的原因可能是:堆空间不足或永生代(java8中的元数据区)不足,并且这时候无法回收一些对象以释放空间,也无法扩容Java对空间。

应用开发人员常犯的错误是在应用中随意维护多个实际并不需要的缓存和对象集合,不必要得增加了应用占用的内存空间,从而导致内存空间不足的错误。下面的这个例子程序比较极端,是为了快速模拟出OOM的错误。

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import java.util.HashMap;
import java.util.Vector;

/**
* 作用: 演示内存溢出的错误,在实际开发中常常犯的一类错误:随意缓存(维护)一些实际不需要的对象的集合,导致内存溢出
* User: duqi
* Date: 2017/12/16
* Time: 14:31
*/
public class MemoryError {

static Vector employeesList;

public static void main(String[] args) {
//雇员对象被保存在两个集合对象中,一个HashMap一个Vector,
//都分别持有Employee和Address的引用对象,这样这些对象就不会被GC当做垃圾回收
employeesList = new Vector();
HashMap employeesMap = new HashMap();
int i = 0;

while (true) {
Emplyee emp1 = new Emplyee("Ram", new Address("MG Road", "Bangalore",
123, "India"));
Emplyee emp2 = new Emplyee("Bob", new Address("House No. 4",
"SCA", 234, "USA"));
Emplyee emp3 = new Emplyee("John", new Address("Church Street",
"Bangalore", 569, "India"));
employeesMap.put(new Integer(i++), emp1);
employeesList.add(emp1);
employeesMap.put(new Integer(i++), emp2);
employeesList.add(emp2);
employeesMap.put(new Integer(i++), emp3);
employeesList.add(emp3);

emp2.addReports(emp1);
emp3.addReports(emp1);
emp3.addReports(emp2);
}

}

}

class Emplyee {
public String name;
public Address address;
public Vector directReports;

public Emplyee(String name, Address address) {
this.name = name;
this.address = address;
directReports = new Vector();
}

public void addReports(Emplyee emplyee) {
directReports.add(emplyee);
}
}

class Address {
public String addr;
public String city;
public int zip;
public String country;

public Address(String addr, String city, int zip, String country) {
this.addr = addr;
this.city = city;
this.zip = zip;
this.country = country;
}
}

在IDEA中运行这个程序,会得到如下的错误信息:

1
2
3
4
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at org.java.learn.jvm.gc.MemoryError.main(MemoryError.java:26)

Process finished with exit code 1

方式方法

在遇到OOM错误时,有多种方法可以分析这个错误:

  • jmap工具,JConsole工具或者JVM启动参数*-XX:+HeapDumpOnOutOfMemorError生成Java堆的快照文件,然后利用jhat或者VisualVM*去分析;
  • 利用SA工具链接到应用程序的进程上去获取对象直方图;
  • 利用JVM参数*-XX:OnOutOfMemoryError*,在遇到OOM的时候自动生成core文件,然后利用SA工具分析core文件生成的对象直方图。

我们这篇文章主要练习如何使用SA,因此不考虑第一种情况(而且这种方法相信大家都比较熟悉),第二种方法,不太符合生产环境的情况(我们不会让你直接暂停线上的应用然后去分析问题),我这里想使用第三种方法。

实践

首先,我尝试通过IDEA设置JVM参数,入下图所示:

2017-12-1717.06.48.png

但是在运行代码后,遇到如下异常,导致无法生成core文件:

2017-12-1615.16.10.png

搜了很多资料,明白应该是mac os默认是不能生成core文件的,这点在终端中通过ulimit -a可以看出:

1
2
3
4
5
6
7
8
9
10
~ ❯❯❯ ulimit -a
-t: cpu time (seconds) unlimited
-f: file size (blocks) unlimited
-d: data seg size (kbytes) unlimited
-s: stack size (kbytes) 8192
-c: core file size (blocks) 0
-v: address space (kbytes) unlimited
-l: locked-in-memory size (kbytes) unlimited
-u: processes 709
-n: file descriptors 4864

最后,我采用如下方式运行上面的例子程序:

  1. 打开一个终端tab,去掉core文件的大小限制:ulimit -c unlimited

  2. 使用javac命令编译上述程序,生成class文件;

  3. 使用sudo java -XX:OnOutOfMemoryError="gcore %p" MemoryError命令运行上述程序,生成的core文件在/core目录下。

    2017-12-1822.56.36.png

  4. 使用Serviceability Agent介绍中提到的方法,启动SA HSDB,并打开上述步骤生成的core文件,使用对象直方图工具生成该程序在发生OOM时候的对象直方图:除去一些JDK自己的类之后,可以看到Address和Emplyee这两个类对应的对象占的空间特别大,这就可以说明这两个对象的空间一直没有被回收。

    2017-12-1616.33.28.png

书上得来终觉浅,绝知此事要躬行。希望这篇文章,能够给你带来一定的帮助,动手试试吧。

参考资料

  1. 《Java性能调优指南》
  2. mac os X生成core文件