知识库

分析 Java 堆转储

本文旨在帮助您使用 Eclipse MAT 浏览获取的堆转储。它涵盖了如何解析大型堆文件以及需要查找的内容。

当您遇到 OutOfMemory 异常时,如果在 neo4j.conf 文件中具有以下设置,它将生成一个 .hprof 文件

dbms.jvm.additional=-XX:+HeapDumpOnOutOfMemoryError
您还可以调整以下设置以指定目录路径,但请确保在发生此类错误时有足够的磁盘空间。
dbms.jvm.additional=-XX:HeapDumpPath=/var/tmp/dumps
dbms.jvm.additional=-XX:OnOutOfMemoryError="tar cvzf /var/tmp/dump.tar.gz /var/tmp/dump;split -b 1G /var/tmp/dump.tar.gz;"

此文件是系统上运行的 Java 进程的堆部分的映像。文件结构取决于您运行 Neo4j 的 JVM 供应商。

Oracle JDK、Open JDK 将生成 hprof 文件,并且可以使用大多数可用工具进行分析。对于 IBM 堆转储,您需要使用 IBM 堆分析器或其他专有工具对其进行解析。

更改 MemoryAnalyzer.ini 中的设置

在您的本地环境中

您需要为进程分配与您拥有的堆转储文件大小一样多的内存。

例如:如果堆大约为 15GB,则分配 17GB。

对于大型堆转储(> 25G),请参见下一节。

编辑 MemoryAnalyzer.ini(在 macOS 上,它位于 /Applications/mat.app/Contents/Eclipse/MemoryAnalyzer.ini 中)

添加或更改设置

-Xms10G
-Xmx25G

在远程机器上

最好将其上传到 AWS/GCP 等具有大量磁盘和 RAM 的实例上。如果您选择 AWS,请使用 竞价型实例

然后,您需要附加 EBS 存储,创建一个 250GB 卷,并将其附加到 EC2 实例。格式化卷并将其挂载到您的 Amazon Linux 实例上。

记下 instanceid 和 storageid 以确保在使用后已正确丢弃资源。

如果堆大约为 61GB,则需要两倍的磁盘空间进行解析。如下所示

$ du -ch java_pid19820*
116M	java_pid19820.a2s.index
5.6G	java_pid19820.domIn.index
 17G	java_pid19820.domOut.index
 61G	java_pid19820.hprof #original heap dump
256K	java_pid19820.i2sv2.index
 11G	java_pid19820.idx.index
 29G	java_pid19820.inbound.index
197M	java_pid19820.index
4.5G	java_pid19820.o2c.index
 12G	java_pid19820.o2hprof.index
 11G	java_pid19820.o2ret.index
 29G	java_pid19820.outbound.index
988K	java_pid19820.threads
 68K	java_pid19820_Component_Report_sel.zip
180G	total
  1. 先决条件安装 Java 并确保有 250GB 的可用空间

  2. 下载适用于 Linux 的 MemoryAnalyzer 工具:下载

  3. 将其解压缩到一个目录中

  4. 编辑 MemoryAnalyzer.ini 以调整 -Xms 和 -Xmx 内存设置

-startup
plugins/org.eclipse.equinox.launcher_1.5.0.v20180512-1130.jar
--launcher.library
plugins/org.eclipse.equinox.launcher.gtk.linux.x86_64_1.1.700.v20180518-1200
-vmargs
-Xms30G
-Xmx100G

在远程机器上解析文件

如果您在本地机器上运行 Eclipse MAT 并且有足够的资源,则此步骤是可选的。如果缺少索引文件,则在打开堆转储文件时将创建它们。

运行 ./ParseHeapDump.sh heapdump.hprof

它位于 Eclipse Mat tar.gz 安装文件的 mat 文件夹中

将您的本地目录与远程目录同步

为了加快速度,您可以使用 rsync 通过 ssh。优点是您可以在发生崩溃时恢复,并且 -z 标志启用压缩。

示例

# on the remote machine
$ mkdir ${REMOTE_DIR}/parsed_files
$ mv *.index ${REMOTE_DIR}/parsed_files/

# on your local machine
$ rsync -P  -e "ssh -i ${PATH_TO_KEY}"  ec2-user@${REMOTE_IP}:${REMOTE_DIR}/heapdump.zip .
$ rsync -Prz  -e "ssh -i ${PATH_TO_KEY}  ec2-user@${REMOTE_IP}:${REMOTE_DIR}/parsed_files/ .

打开 Eclipse MAT

要打开堆转储,请转到文件 > 打开堆转储(不是获取堆转储)并浏览到您的堆转储位置。

无需打开现有报告,如果您有模式对话框,请按取消。

在“概述”选项卡中,左键单击最大的对象。

选择“列出对象”>“具有传出引用”。

它将打开一个包含所有元素列表的新选项卡。

展开第一级,然后展开第二级的全部内容。

Cypher 查询字符串

堆转储中有很多对象,无需遍历 Object[]、byte[]、Strings 等。

您可能希望过滤包含 PreParsed 的类。找到后,列出它们的传出引用以交叉检查实例最多的引用。将打开一个新选项卡,您将能够看到 Cypher 查询的 rawStatement。

检查线程转储

使用在堆转储之前获取的线程转储

垃圾收集器将无法收集线程对象,直到线程系统也取消对对象的引用,如果线程处于活动状态,则不会发生这种情况。

因此,如果堆中有大量内存,则应该有一个可能长时间运行的线程与您的大型对象相关联。

要查找它,请在线程转储中查找线程名称。

$ grep neo4j.BoltWorker-394 *

5913-tdump-201903291746.log:"neo4j.BoltWorker-394 [bolt]" #620 daemon prio=5 os_prio=0 tid=0x00007fb737619800 nid=0x8cec waiting on condition [0x00007fb38d00f000]
5913-tdump-201903291751.log:"neo4j.BoltWorker-394 [bolt] [/www.xxx.yyy.zzz:57570] " #620 daemon prio=5 os_prio=0 tid=0x00007fb737619800 nid=0x8cec runnable [0x00007fb38d00b000]
5913-tdump-201903291756.log:"neo4j.BoltWorker-394 [bolt] [/www.xxx.yyy.zzz:57570] " #620 daemon prio=5 os_prio=0 tid=0x00007fb737619800 nid=0x8cec runnable [0x00007fb38d00b000]

请注意,线程转储包含在堆转储中。它们以纯文本格式存在于文件中,但您在 Eclipse Mat 中没有 STATE 信息。您可以使用其他工具(例如 VisualVM)获得它们

$ head -10 java_pid19820.threads
Thread 0x7fd64b0e1610
  at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.addConditionWaiter()Ljava/util/concurrent/locks/AbstractQueuedSynchronizer$Node; (AbstractQueuedSynchronizer.java:1855)
  at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(J)J (AbstractQueuedSynchronizer.java:2068)
  at java.util.concurrent.LinkedBlockingQueue.poll(JLjava/util/concurrent/TimeUnit;)Ljava/lang/Object; (LinkedBlockingQueue.java:467)
  at com.hazelcast.util.executor.CachedExecutorServiceDelegate$Worker.run()V (CachedExecutorServiceDelegate.java:210)
  at java.util.concurrent.ThreadPoolExecutor.runWorker(Ljava/util/concurrent/ThreadPoolExecutor$Worker;)V (ThreadPoolExecutor.java:1149)
  at java.util.concurrent.ThreadPoolExecutor$Worker.run()V (ThreadPoolExecutor.java:624)
  at java.lang.Thread.run()V (Thread.java:748)
  at com.hazelcast.util.executor.HazelcastManagedThread.executeRun()V (HazelcastManagedThread.java:76)
  at com.hazelcast.util.executor.HazelcastManagedThread.run()V (HazelcastManagedThread.java:92)