JVM Memory Analysis

Table of Contents

1. OutOfMemoryError 错误简介

在 Java 中, 开发者不用直接控制程序运行内存, JVM 有垃圾回收器对内存区域管理。尽管如此,OutOfMemoryError 是常见的 Java 错误,有多种不同原因可以导致 java.lang.OutOfMemoryError,如:

java.lang.OutOfMemoryError: Java heap space
java.lang.OutOfMemoryError: GC Overhead limit exceeded
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
......

不同类型的 OutOfMemoryError,有不同原因及解决办法,可参考:Java Platform, Standard Edition Troubleshooting Guide

1.1. OutOfMemoryError 实例程序

下面是一个演示程序,它会导致 OutOfMemoryError:

import java.util.ArrayList;
import java.util.List;

// -Xms40m -Xmx40m -XX:+HeapDumpOnOutOfMemoryError
public class OutOfMemoryTest {
    static private List<MyClassA> list = new ArrayList<MyClassA>();

    public static void main(String[] args) {
        while (true) {                 // 不停地往list中增加MyClassA对象
            list.add(new MyClassA());
        }
    }
}

class MyClassA {
}

上面程序的可能运行结果:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid33381.hprof ...
Heap dump file created [60995402 bytes in 0.252 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3210)
	at java.util.Arrays.copyOf(Arrays.java:3181)
	at java.util.ArrayList.grow(ArrayList.java:261)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
	at java.util.ArrayList.add(ArrayList.java:458)
	at OutOfMemoryTest.main(OutOfMemoryTest.java:10)

1.2. 实例:用 jmap 检查内存占用基本情况

jmap -heap <pid> 可检查内存占用基本情况,如:

$ jmap -heap 27118
Attaching to process ID 27118, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.112-b16

using thread-local object allocation.
Parallel GC with 4 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4294967296 (4096.0MB)
   NewSize                  = 89128960 (85.0MB)
   MaxNewSize               = 1431306240 (1365.0MB)
   OldSize                  = 179306496 (171.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 213909504 (204.0MB)
   used     = 0 (0.0MB)
   free     = 213909504 (204.0MB)
   0.0% used
From Space:
   capacity = 254279680 (242.5MB)
   used     = 254279680 (242.5MB)
   free     = 0 (0.0MB)
   100.0% used
To Space:
   capacity = 365428736 (348.5MB)
   used     = 0 (0.0MB)
   free     = 365428736 (348.5MB)
   0.0% used
PS Old Generation
   capacity = 1231552512 (1174.5MB)
   used     = 1157770520 (1104.1360092163086MB)
   free     = 73781992 (70.3639907836914MB)
   94.0090259017717% used

734 interned Strings occupying 49224 bytes.

1.3. 实例:用 jmap 检查哪些 Java 实例占用较大内存

jmap -histo:live <pid> 可分析 Java 实例数和实例所占内存大小,如:

$ jmap  -histo:live 28208

 num     #instances         #bytes  class name
----------------------------------------------
   1:      64213348     1027413568  MyClassA
   2:           524      280390160  [Ljava.lang.Object;
   3:          1030          94200  [C
   4:           482          54968  java.lang.Class
   5:            10          25024  [B
   6:          1018          24432  java.lang.String
   7:            79           5688  java.lang.reflect.Field
   8:           256           4096  java.lang.Integer
......

注:“[C”代表字符数组,“[B”表示布尔数组。

2. Eclispe MAT(Memory Analyzer Tool)

Eclispe MAT(Memory Analyzer Tool) 是一个强大的 Java 内存分析工具,强烈推荐使用。

Eclispe MAT 即是一个 Eclipse 插件,也提供了单独的安装程序。如果是以 Eclipse 插件形式安装,则点击“Window > Perspective > Open Perspective > Other ... > Memory Analysis”可以找到它。

参考:
Basic Tutorial: http://help.eclipse.org/kepler/index.jsp?topic=%2Forg.eclipse.mat.ui.help%2Fgettingstarted%2Fbasictutorial.html

2.1. 生成 Heap dump 文件

使用 Eclispe MAT,首先你需要有 Heap dump 文件,有多种方法可以生成 Heap dump 文件。如:
方法一:启动 jvm 时指定下面选项: -XX:+HeapDumpOnOutOfMemoryError ,使用这种方法只有发生 OutOfMemoryError 错误时才会产生 heap dump 文件。
方法二(推荐方式),使用 jcmd <pid> GC.heap_dump <full_path_of_file>
方法三:使用 jmap -dump:format=b,file=myfile1.hprof <pid>

注: jcmd 还有很多其它功能,使用 jcmd <pid> help 可查看 jcmd 的所有支持命令。

2.2. Dominator Tree

The dominator tree displays the biggest objects in the heap dump.

java_eclipse_mat_dominator_tree.png

Figure 1: Dominator Tree in Eclispe MAT

2.2.1. Shallow Heap vs. Retained Heap

在 Dominator Tree 中,会显示 Shallow Heap 和 Retained Heap 的大小(如图 1 所示)。它们的含义分别如下:
Shallow Heap:对象自身占用的内存大小,不包括它引用的对象。
Retained Heap:当前对象大小加上当前对象可直接或间接引用到的对象的大小总和。也就是说, Retained Heap 就是当前对象被 GC 后,从 Heap 上总共能释放掉的内存。

在分析 Java 内存占用过多的问题时,观察 Retained Heap 的大小更有意义,Dominator Tree 中默认就是把 Retained Heap 较大的记录排在前面。

2.3. Path to GC Roots

我们可以通过“Path to GC Roots”(如图 2 所示)功能查看可疑对象到 GC Roots 的引用链。于是就能找到可疑对象是通过怎样的路径与 GC Roots 相关联并导致垃圾回收器无法自动回收它们的,这样可以比较准确地定位出泄露代码的位置。

java_eclipse_mat_path2root.png

Figure 2: Path to GC Roots

Author: cig01

Created: <2018-02-24 Sat>

Last updated: <2018-05-17 Thu>

Creator: Emacs 27.1 (Org mode 9.4)