理解内存消耗
你可能已经将 Neo4j 配置为使用 4GB 堆内存和 6GB 页缓存,然后高枕无忧,以为 Java 进程在你 12GB 内存的机器上不会超过 10GB,结果却发现 Neo4j 发生了 OOM 错误并崩溃了。幕后发生了什么?为什么 Neo4j 消耗的内存比你分配的要多?这是内存泄漏还是正常行为?疑问重重!让我们尝试回答其中的一些问题,以便你在涉及内存时不会措手不及。
虽然内存泄漏确实可能发生,但更多情况下,较高的内存消耗是 JVM 的正常行为。为了正常运行,JVM 需要在其他几个类别中分配更多内存。JVM 内存最显著的类别包括
-
Heap(堆) - 堆是存储你的 Class 实例化对象或“Objects”的地方。
-
Thread stacks(线程栈) - 每个线程都有自己的 call stack(调用栈)。stack(栈)存储基本类型的局部变量和 object references(对象引用),以及 call stack(方法调用列表)本身。当 stack frames(栈帧)超出上下文时,stack 会被清理,因此这里不执行 GC。
-
Metaspace(在较旧的 Java 版本中称为 PermGen) - Metaspace 存储你的 Objects 的 Class definitions(类定义)以及其他一些 metadata(元数据)。
-
Code cache(代码缓存) - JIT compiler(JIT 编译器)将其生成的 native code(本地代码)存储在 code cache 中,通过重用来提高性能。
-
Garbage collection(垃圾回收) - 为了让 GC 知道哪些 objects 符合回收条件,它需要跟踪 object graphs(对象图)。因此,这是用于此内部簿记而消耗的一部分内存。
-
Buffer pools(缓冲区池) - 许多库和框架在 heap 之外分配 buffers,以提高性能。这些 buffer pools 可用于在 Java code 和 native code 之间共享内存,或将文件的区域映射到内存中。
除了上面列出的原因之外,你还可能因其他原因占用内存,但我只想让你知道,JVM 内部结构会占用大量内存。
我需要担心所有这些吗?
让我们从整体上看!
配置 Neo4j 的内存时,你可能会遇到许多术语,例如 on-heap、off-heap、page cache、direct memory、OS memory 等等……这一切意味着什么?配置内存时应该注意什么?首先,让我们从理解其中一些术语开始
-
Heap(堆):JVM 有一个 heap,它是运行时数据区域,用于分配所有 class instances(类实例)和 arrays(数组)的内存。objects 的 Heap storage 通过自动存储管理系统(称为 garbage collector 或 GC)进行回收。
-
Off-Heap(堆外):有时 heap memory 不够用,尤其是当我们需要在不增加 GC pauses 的情况下缓存大量数据、在 JVMs 之间共享缓存数据或在内存中添加一个能抵御 JVM 崩溃的 persistence layer(持久层)时。在所有提到的情况下,off-heap memory 都是可能的解决方案之一。由于 off-heap 存储在内存中继续管理,它比 on-heap 存储略慢,但仍然比 disk store(磁盘存储)快(并且也不受 GC 影响)。
-
Page cache(页缓存):page cache 位于 off-heap,用于缓存 Neo4j data(以及 native indexes)。将 graph data 和 indexes 缓存到 memory 中有助于避免昂贵的 disk access 并带来最佳性能。
heap 和 off-heap 是通用的 Java 术语,而 page cache 特指 Neo4j 的原生缓存。 |
下面是这一切如何组合在一起的图示

如上所示,我们可以将 Neo4j 的内存消耗分为 2 个主要区域:On-heap 和 off-heap
On-heap 是 runtime data 的所在地,也是 query execution(查询执行)、graph management(图管理)和 transaction state1(事务状态)存在的地方。
将 heap 设置为最佳值本身就是一个棘手的任务,本文的目的不是涵盖这部分内容,而是整体理解 Neo4j 的内存消耗。 |
Off-heap 本身可以分为 3 个类别。我们不仅有 Neo4j 的 page cache(负责将 graph data 缓存到 memory 中),还有 JVM 运行所需的所有其他内存(JVM Internals)。你看到的剩余部分是 direct memory,我们稍后会讲到。
在 Neo4j 中,有三个你可以配置的内存设置:initial heap size (-Xms)、maximum heap size (-Xmx) 和 page cache。实际上,前两个影响的是同一个内存空间,因此 Neo4j 只允许你配置上面提到的所有内存类别中的两种。不过,你可以对这两种进行限制。将最大 heap 设置为 4GB,page cache 设置为另外 4GB,可以保证这些特定组件不会超过这个大小。那么,你的进程怎么会消耗比设定值更多的内存呢? 一个常见的误解是,通过设置 heap 和 page cache,Neo4j 的进程内存消耗就不会超出范围,但更有可能的是 Neo4j 的实际内存占用量更大。
正如我们在上面看到的,JVM 需要一些额外的内存才能正常运行。例如,在高度并发的环境中运行意味着 thread stack 占用的内存将等于 JVM 处理的 concurrent threads 的数量乘以 thread stack size (-Xss)。
还存在一些更难发现的非 heap 内存使用来源,例如 buffer pools。这就引入了 direct memory。Direct byte buffers 对于提高性能很重要,因为它们允许 native code 和 Java code 在不复制数据的情况下共享数据。但这代价高昂,这意味着 byte buffers 一旦创建通常会被重用。因此,一些框架会在进程的整个生命周期内保留它们。一个例子是 Netty。Neo4j 使用 Netty(一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端,https://netty.java.net.cn/),它是目前 direct memory 的主要使用者。它主要用于 buffering 和 IO,但 Neo4j 中的多个组件也使用它。
在几乎所有情况下,你的 direct memory 使用量都不会增长到出现问题的水平,但在非常极端和要求苛刻的使用场景中(例如:当我们有大量的 concurrent accesses 和 updates 时),你可能会开始看到 Neo4j 的进程消耗的内存远多于我们配置的内存,或者你也可能会遇到与 direct memory 相关的 Out of Memory 错误。
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) ...
这些症状与 direct memory 增长有关。虽然我们不管理 Netty 使用的内存,但可以通过 JVM 设置来限制 Neo4j(以及任何 Java 进程)可以使用的 direct memory:-XX:MaxDirectMemorySize
。这与 neo4j.conf
文件中的 dbms.jvm.additional=-Dio.netty.maxDirectMemory=0
结合使用。这将强制 Netty 使用 direct memory 设置,从而有效限制其增长。
这些是敏感设置,会影响 NEO4J 的正常功能。请在未咨询 Neo4j 专业人士的情况下不要更改这些设置。如果你在使用 Direct Memory 时遇到问题,可以提交一个支持工单,我们将尽力提供最佳建议。 |
索引
根据你使用的是 Lucene 还是 native indexes,它们占用的内存将位于不同的位置。如果你使用的是 Lucene indexes,它们将位于 off-heap,我们无法控制它们使用了哪些内存。在上面的图片中,它们将与 page cache 并列存在,但在一个非托管的块中。
如果你使用的是 native indexes,它们占用的内存将位于 page cache 内部,这意味着我们可以一定程度上控制它们可以占用的内存量。在设置 page cache 大小时应考虑到这一点。
监控
现在你一定已经意识到内存配置并非易事。有什么可以让你更轻松的吗?你可以使用 Native Memory Tracking,这是一个 JVM 特性,用于跟踪内部内存使用情况。要启用它,你需要将以下内容添加到你的 neo4j.conf
文件中
dbms.jvm.additional=-XX:NativeMemoryTracking=detail
然后获取 Neo4j 的 PID,并使用 jcmd
通过命令 jcmd <PID> VM.native_memory summary
打印出进程的 native memory 使用情况。你将获得内存中每个类别的详细分配信息,如下所示
$ 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
dump 本身的作用有限。更常见的方法是获取多个 dump 并通过运行 jcmd <PID> VM.native_memory summary.diff
来进行比较。
这是一个调试内存问题的好工具。
1 从 3.5 版本开始,transaction state 也可以配置为与 heap 分开分配。 |
此页面有帮助吗?