理解内存消耗
假设您已将 Neo4j 配置为使用 4GB 的堆内存和 6GB 的页面缓存,并轻松地坐下来,认为 Java 进程在您的 12GB 机器上不会超过 10GB,但随后意识到 Neo4j 发生了 OOM 错误并崩溃了。幕后发生了什么?为什么 Neo4j 消耗的内存比您分配的要多?这是内存泄漏还是正常行为?这么多问题!让我们尝试回答其中一些问题,以便您在遇到内存问题时不会措手不及。
虽然可能会发生内存泄漏,但通常情况下,较高的内存消耗是 JVM 的正常行为。为了正常运行,JVM 需要在其他几个类别中分配更多内存。JVM 内存最重要的类别是
-
堆 - 堆是存储类实例或“对象”的地方。
-
线程栈 - 每个线程都有自己的调用栈。栈存储原始局部变量和对象引用以及调用栈(方法调用列表)本身。栈在栈帧移出上下文时被清理,因此此处不执行 GC。
-
元空间(在旧版本的 Java 中称为 PermGen) - 元空间存储对象的类定义以及其他一些元数据。
-
代码缓存 - JIT 编译器将生成的原生代码存储在代码缓存中,以通过重复使用代码来提高性能。
-
垃圾回收 - 为了让 GC 了解哪些对象有资格进行回收,它需要跟踪对象图。因此,这是分配给此内部簿记的一部分内存。
-
缓冲池 - 许多库和框架在堆之外分配缓冲区以提高性能。这些缓冲池可用于在 Java 代码和原生代码之间共享内存,或将文件的区域映射到内存中。
除了上面列出的原因之外,您最终可能会因为其他原因而使用内存,但我只想让您意识到 JVM 内部消耗了大量内存。
我需要担心所有这些吗?
让我们对此进行透视!
在配置 Neo4j 的内存时,您可能会遇到许多术语,例如堆内、堆外、页面缓存、直接内存、操作系统内存……这些都意味着什么?在配置内存时应该注意什么?首先,让我们从了解其中一些术语开始
-
堆:JVM 具有一个堆,它是运行时数据区域,从中分配所有类实例和数组的内存。对象的堆存储由自动存储管理系统(称为垃圾回收器或 GC)回收。
-
堆外:有时堆内存不足,尤其是在我们需要缓存大量数据而不增加 GC 暂停、在 JVM 之间共享缓存数据或在内存中添加一个抗 JVM 崩溃的持久层时。在所有提到的情况下,堆外内存都是可能的解决方案之一。由于堆外存储继续在内存中管理,因此它比堆内存储略慢,但仍比磁盘存储快(并且不受 GC 影响)。
-
页面缓存:页面缓存位于堆外,用于缓存 Neo4j 数据(和原生索引)。将图形数据和索引缓存到内存中将有助于避免代价高昂的磁盘访问并带来最佳性能。
虽然堆和堆外是通用的 Java 术语,但页面缓存是指 Neo4j 的原生缓存。 |
以下是所有这些如何组合在一起的示意图

如上所示,我们可以将 Neo4j 的内存消耗分为两个主要区域:堆内和堆外
堆内是运行时数据所在的地方,也是查询执行、图管理和事务状态1存在的地方。
将堆设置为最佳值本身就是一项棘手的任务,本文的目的是了解 Neo4j 的整体内存消耗,而不是涵盖此内容。 |
堆外本身可以分为三类。我们不仅有 Neo4j 的**页面缓存**(负责将图形数据缓存到内存中),还有 JVM 工作所需的所有其他内存(**JVM 内部**)。您在那里看到的剩余块是**直接内存**,我们稍后会详细介绍。
在 Neo4j 中,您可以配置三个内存设置:初始堆大小(-Xms)、最大堆大小(-Xmx)和页面缓存。实际上,前两个影响相同的内存空间,因此 Neo4j 仅允许您配置上述所有内存类别中的两个。不过,这些是可以施加限制的。将最大堆设置为 4GB,并将页面缓存设置为另一个 4GB,可以保证这些特定组件不会增长到超过该大小。那么您的进程如何消耗超过设置的值呢?**一个常见的误解是,通过设置堆和页面缓存,Neo4j 的进程内存消耗不会超过此值,但 Neo4j 的内存占用量很可能更大。**
好吧 - 正如我们上面所看到的 - JVM 将需要一些额外的内存才能正常运行。例如,运行高并发环境意味着线程栈占用的内存将等于 JVM 处理的并发线程数乘以线程栈大小(-Xss)。
还有一些难以找到的非堆内存使用来源,例如缓冲池。输入**直接内存**。直接字节缓冲区对于提高性能非常重要,因为它们允许原生代码和 Java 代码共享数据而无需复制它。但是这很昂贵,这意味着字节缓冲区通常在创建后会重复使用。结果,一些框架在进程生命周期内保留它们。一个例子是 Netty。Neo4j 使用 Netty(一个用于快速开发易于维护的高性能协议服务器和客户端的异步事件驱动网络应用程序框架,https://netty.java.net.cn/),这是直接内存的主要用户。它主要用于缓冲和 IO,但 Neo4j 中的多个组件都使用了它。
在几乎所有情况下,您的直接内存使用量都不会增长到问题级别,但在非常极端和苛刻的使用情况下(即:当我们有大量的并发访问和更新时),您可能会开始看到 Neo4j 的进程消耗的内存远超我们配置的内存,或者您也可能会收到一些关于直接内存的内存不足错误
2018-11-14 09:32:49.292+0000 ERROR [o.n.b.t.SocketTransportHandler] Fatal error occurred when handling a client connection: failed to allocate 16777216 byte(s) of direct memory (used: 6442450944, max: 6442450944) failed to allocate 16777216 byte(s) of direct memory (used: 6442450944, max: 6442450944) io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 16777216 byte(s) of direct memory (used: 6442450944, max: 6442450944) at io.netty.util.internal.PlatformDependent.incrementMemoryCounter(PlatformDependent.java:624) at io.netty.util.internal.PlatformDependent.allocateDirectNoCleaner(PlatformDependent.java:578) at io.netty.buffer.PoolArena$DirectArena.allocateDirect(PoolArena.java:718) at io.netty.buffer.PoolArena$DirectArena.newChunk(PoolArena.java:707) ...
这些症状与直接内存增长有关。虽然我们不管理 Netty 使用的内存,但可以通过 JVM 设置限制 Neo4j(以及任何 Java 进程)可以使用多少直接内存:-XX:MaxDirectMemorySize
。这与 neo4j.conf
文件中的 dbms.jvm.additional=-Dio.netty.maxDirectMemory=0
结合使用。这将强制 Netty 使用直接内存设置,从而有效地限制其增长量。
这些是敏感设置,会影响 Neo4j 的正常功能。请勿在未咨询 Neo4j 专业人员的情况下更改这些设置。如果您遇到直接内存问题,只需记录支持工单,我们将尽力为您提供建议。 |
索引
根据您使用的是 Lucene 索引还是原生索引,这些索引占用的内存将位于不同的位置。如果您使用的是 Lucene 索引,则这些索引将位于堆外,我们无法控制它们使用多少内存。在上图中,它们将与页面缓存一起位于未管理的块中。
如果您使用的是原生索引,则它们占用的内存将位于页面缓存**内部**,这意味着我们可以一定程度上控制它们可以占用多少内存。在设置页面缓存大小时,应考虑这一点。
监控
到目前为止,您必须已经意识到内存配置并非那么简单。有什么可以使您的生活更轻松?您可以使用原生内存跟踪,这是一个 JVM 功能,用于跟踪内部内存使用情况。要启用它,您需要将以下内容添加到您的 neo4j.conf
文件中
dbms.jvm.additional=-XX:NativeMemoryTracking=detail
然后获取 Neo4j 的 PID,并使用jcmd
打印出进程的本地内存使用情况,使用jcmd <PID> VM.native_memory summary
。您将获得内存中每个类别的详细分配信息,如下所示
$ jcmd <PID> VM.native_memory summary
Native Memory Tracking:
Total: reserved=3554519KB, committed=542799KB
- Java Heap (reserved=2097152KB, committed=372736KB)
(mmap: reserved=2097152KB, committed=372736KB)
- Class (reserved=1083039KB, committed=38047KB)
(classes #5879)
(malloc=5791KB #6512)
(mmap: reserved=1077248KB, committed=32256KB)
- Thread (reserved=22654KB, committed=22654KB)
(thread #23)
(stack: reserved=22528KB, committed=22528KB)
(malloc=68KB #116)
(arena=58KB #44)
- Code (reserved=251925KB, committed=15585KB)
(malloc=2325KB #3622)
(mmap: reserved=249600KB, committed=13260KB)
- GC (reserved=82398KB, committed=76426KB)
(malloc=5774KB #182)
(mmap: reserved=76624KB, committed=70652KB)
- Compiler (reserved=139KB, committed=139KB)
(malloc=9KB #128)
(arena=131KB #3)
- Internal (reserved=6127KB, committed=6127KB)
(malloc=6095KB #7439)
(mmap: reserved=32KB, committed=32KB)
- Symbol (reserved=9513KB, committed=9513KB)
(malloc=6724KB #60789)
(arena=2789KB #1)
- Native Memory Tracking (reserved=1385KB, committed=1385KB)
(malloc=121KB #1921)
(tracking overhead=1263KB)
- Arena Chunk (reserved=186KB, committed=186KB)
(malloc=186KB)
通常,jcmd
转储本身仅具有一定的参考价值。更常见的情况是获取多个转储并通过运行jcmd <PID> VM.native_memory summary.diff
进行比较。
这是一个用于调试内存问题的绝佳工具。
1 从 3.5 版本开始,事务状态也可以配置为与堆分开分配。 |
此页面是否有帮助?