Java堆外内存不直接受GC管理,仅通过DirectByteBuffer对象的GC间接触发Cleaner释放;若对象被强引用持有、GC不及时或Cleaner线程阻塞,将导致堆外内存泄漏。

不能。OpenJDK 的标准垃圾回收器(如 G1、ZGC、Shenandoah、Parallel 等)**完全不感知、也不管理堆外内存**,包括 ByteBuffer.allocateDirect()、Unsafe.allocateMemory() 或 JNI 分配的本地内存。
为什么 GC 不回收堆外内存
堆外内存(Direct Memory)位于 JVM 堆之外,由操作系统直接管理,JVM 的 GC 仅作用于 Java 堆(Heap)和元空间(Metaspace),其可达性分析、标记、清理逻辑均不覆盖堆外地址空间。
- GC Roots 不包含对堆外内存块的直接引用——哪怕一个
DirectByteBuffer对象在堆内持有着 native address,GC 也只负责回收这个DirectByteBuffer实例本身,不自动释放它背后那块 native memory - 堆外内存的生命周期依赖 Java 对象的“间接控制”:例如
DirectByteBuffer构造时会通过Cleaner注册清理动作,但该动作是否执行、何时执行,受 GC 触发时机与 Reference 处理队列调度影响,并非强保障 -
System.gc()可能加速Cleaner执行,但不保证立即释放;且生产环境通常禁用System.gc(),避免诱发 Full GC
堆外内存实际靠什么回收
依赖三类机制协同,但都**不是 GC 主动回收**:
-
Cleaner:Java 9+ 中DirectByteBuffer使用Cleaner(基于PhantomReference)注册释放逻辑,GC 发现对象不可达后,将其加入ReferenceQueue,由后台线程异步调用clean()—— 但该线程可能延迟、阻塞或被抑制(如高负载下) - 显式调用
Buffer.clear()/Buffer.compact()不释放内存;必须调用((DirectBuffer) buf).cleaner().clean()(不推荐,属内部 API)或确保buf被及时 GC 且Cleaner线程正常工作 - Native 层泄漏时,连
Cleaner都无法触发:比如用Unsafe.allocateMemory()后未手动配对Unsafe.freeMemory(),或 JNI 中 malloc 未 free,这类内存彻底脱离 JVM 管控
常见 OOM 错误和排查线索
当堆外内存耗尽,抛出的是 OutOfMemoryError: Direct buffer memory,而非堆内存的 java.lang.OutOfMemoryError: Java heap space。
- 默认堆外内存上限由
-XX:MaxDirectMemorySize控制(不设则等同于-Xmx值),超限即抛异常,但该限制是“软性闸门”,不解决泄漏问题 - JVM 进程 RSS 内存持续上涨,而
jstat -gc显示堆使用平稳,jcmd <pid> VM.native_memory summary</pid>中Internal或Direct区域飙升,基本可定位为堆外泄漏 -
DirectByteBuffer实例本身很小(约百字节),但每个背后挂载几 MB 的 native memory,用jmap -histo看不到内存占用真相,需结合native_memory和堆 dump 中的DirectByteBuffer引用链分析
堆外内存的“回收”本质是资源归属权移交,不是 GC 的职责范围;依赖 Cleaner 的异步性、Reference 处理线程的健康度、以及开发者对 native 资源生命周期的显式把控——任何一环断裂,就会变成静默泄漏。


















