Java 堆外内存 DirectBuffer 的分配与回收
Java 堆外内存(Off-Heap Memory)是指直接在 JVM 堆之外分配的内存,主要由操作系统管理。使用 DirectBuffer(特别是 DirectByteBuffer)操作堆外内存,可以避免数据在 Java 堆和本地堆之间复制,从而显著提升 I/O 性能。但这也带来了内存分配与回收的特殊性,处理不当极易引发内存泄漏。
一、堆外内存的分配机制
堆外内存的分配并不像 Java 对象那样简单,它涉及 JVM 内部的权限检查和本地方法的调用。
-
调用分配 API
在代码中,使用java.nio.ByteBuffer类的静态方法来申请堆外内存。输入以下代码申请 10MB 的直接内存:
ByteBuffer buffer = ByteBuffer.allocateDirect(10 * 1024 * 1024);这行代码会在 Java 堆内创建一个
DirectByteBuffer对象,但实际的数据存储空间位于堆外。 -
校验内存额度
JVM 内部维护一个Bits类,用于记录当前已使用的堆外内存总量。在真正分配前,系统会检查申请的大小加上已使用量是否超过限制。-
默认限制:默认情况下,最大堆外内存大小约等于最大堆内存(
-Xmx)。部分 VM(如 JRockit)默认可能无限制,HotSpot 默认约为 64MB 或与堆一致。 -
自定义限制:添加 JVM 启动参数来显式指定最大值:
-XX:MaxDirectMemorySize=512m
-
-
尝试分配与回收尝试
如果校验发现内存不足,JVM 会尝试通过System.gc()触发一次垃圾回收,试图释放掉之前被废弃但未回收的DirectByteBuffer对象,并休眠 100毫秒。如果回收后内存仍不足,系统将抛出异常:
java.lang.OutOfMemoryError: Direct buffer memory -
执行本地分配
如果校验通过,JVM 将调用Unsafe类的本地方法allocateMemory,向操作系统申请内存。申请成功后,JVM 会将这块内存清零,并将内存地址存储在DirectByteBuffer对象中供后续访问。
二、堆外内存的回收机制
堆外内存不受 Java GC 的直接管控,其回收依赖于 Java 堆对象的回收机制和虚引用技术。
1. 自动回收原理
当 DirectByteBuffer 对象在 Java 堆中不再被引用时,它会被当作垃圾。但此时它对应的堆外内存并不会立即释放,必须通过 Cleaner 机制来处理。
下面是 DirectByteBuffer 从被废弃到堆外内存释放的完整流程:
- 虚引用:
DirectByteBuffer在创建时会关联一个Cleaner对象(继承自虚引用PhantomReference)。虚引用不影响对象的生命周期,也无法通过它获取对象实例。 - ReferenceHandler 线程:JVM 启动时会创建一个高优先级的
ReferenceHandler守护线程。该线程不断监控引用队列,一旦发现Cleaner对象被加入队列,就执行其clean方法。 - Deallocator:
clean方法最终调用Deallocator的run逻辑,利用Unsafe.freeMemory释放本地内存。
2. 手动回收(不推荐)
在某些极端性能敏感场景下,若确定堆外内存不再使用,可以通过反射手动调用 Cleaner 的 clean 方法来提前释放内存,但这破坏了 JVM 的自动管理机制,通常不建议使用。
三、System.gc() 的关键作用与陷阱
在分配堆外内存的 Bits.reserveMemory 方法中,如果内存不足,代码会显式调用 System.gc()。这是一个关键的设计细节,但也埋下了隐患。
-
显式 GC 的必要性
由于堆外内存的释放依赖于 Java 堆对象的 GC(即先回收DirectByteBuffer对象,才能触发Cleaner释放堆外内存),因此在分配新内存失败时,主动建议 JVM 进行 GC 是合理的自救措施。 -
DisableExplicitGC 的风险
为了减少 GC 对性能的影响,很多线上环境会配置参数-XX:+DisableExplicitGC,该参数会禁用代码中的System.gc()调用。- 后果:如果开启了此参数,当堆外内存不足时,
Bits.reserveMemory中的System.gc()调用将失效。JVM 不会尝试回收旧的DirectByteBuffer,而是直接抛出OutOfMemoryError。 - 现象:此时 Java 堆内存可能还很充裕,但物理内存已被耗尽。
建议:如果应用大量使用了 NIO(如 Netty、RocketMQ 等),请慎用
-XX:+DisableExplicitGC,或者使用-XX:+ExplicitGCInvokesConcurrent将显式 GC 转换为并发 GC 以降低停顿。 - 后果:如果开启了此参数,当堆外内存不足时,
四、堆外内存溢出的排查
当遇到 java.lang.OutOfMemoryError: Direct buffer memory 或物理内存异常飙升时,执行以下步骤进行排查。
-
区分内存区域
首先确认是 Java 堆溢出还是堆外溢出。使用jmap -heap <pid>查看堆内存使用情况。如果堆内存使用率很低,但进程占用内存很高,则极有可能是堆外内存泄漏。 -
强制触发 GC 验证
由于常规工具(如 VisualVM)无法直接监控堆外内存,可以利用 GC 机制来验证。运行以下命令:
jmap -histo:live <pid>该命令会强制 JVM 执行一次 Full GC。
-
观察内存变化
在执行上述命令的同时,观察操作系统的内存监控工具(如top或top -p <pid>的 RES 指标)。- 如果 GC 后,操作系统的内存占用明显下降,说明释放了大量堆外内存,这些内存对应的
DirectByteBuffer对象此前未被回收。 - 如果 GC 后内存占用不变,则可能存在非
DirectByteBuffer管理的堆外内存泄漏(如通过 JNI 调用的 native 代码泄漏),或者-XX:MaxDirectMemorySize设置确实过小。
- 如果 GC 后,操作系统的内存占用明显下降,说明释放了大量堆外内存,这些内存对应的
-
分析堆转储
抓取堆转储文件(Heap Dump),分析java.nio.DirectByteBuffer实例的数量。如果存在大量未回收的实例,需检查业务代码中是否存在未及时释放引用的情况,或者是否由于对象生命周期过长导致无法进入 Old Gen 的 GC 范围。

暂无评论,快来抢沙发吧!