directbytebuffer堆外内存回收依赖cleaner虚引用机制:当堆内对象被gc判定不可达后,referencehandler线程从引用队列取出cleaner并调用clean()执行unsafe.freememory,该过程异步且延迟不可控。

堆外内存不归 GC 管,但回收依赖 GC 触发
当你调用 `ByteBuffer.allocateDirect(1024)`,JVM 在堆内创建一个 `DirectByteBuffer` 实例(几十字节),同时用 `Unsafe.allocateMemory()` 向操作系统申请 1KB 堆外内存。这两者是分离的:
- 堆内对象受 GC 管理,但体积小,可能长期存活(尤其被意外强引用)
- 堆外内存不受 GC 控制,也不会自动释放
- 只有当 `DirectByteBuffer` 对象被 GC 判定为不可达并回收时,才会触发关联的 `Cleaner` 执行清理
Cleaner 是虚引用驱动的异步回收器
`Cleaner` 是 `PhantomReference` 的子类,它不阻止对象回收,只在对象被回收后收到通知:
- 每个 `DirectByteBuffer` 构造时,会调用 `Cleaner.create(this, new Deallocator(...))`,绑定一个清理任务
- 该任务封装了 `unsafe.freeMemory(address)` 调用所需的地址和大小
- GC 完成后,`ReferenceHandler` 后台线程从引用队列中取出已入队的 `Cleaner`,并调用其 `clean()` 方法
- 整个过程是异步的,延迟不可控(可能几百毫秒到数秒,甚至更久)
生产环境不能靠 System.gc() 强制推进回收
很多老代码会写 `buffer = null; System.gc();` 来“加速”释放,但这在 CMS 或 Serial GC 下极易引发长时间 STW,导致请求堆积、超时、系统假死:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- `System.gc()` 默认触发 Full GC,停顿时间随堆大小线性增长
- 而 `Cleaner` 清理本身不依赖 Full GC —— 只要对象进入 FinalReference 队列(Minor GC 也可能触发部分 Cleaner 处理),就有可能被执行
- 频繁调用 `System.gc()` 反而制造 GC 压力,加剧 OOM 风险
推荐的主动回收方式:反射调 clean() + 显式置空
若业务场景明确知道某块 `DirectByteBuffer` 已用完(如大文件解析、临时序列化缓冲),可绕过 GC 主动释放:
- 确保 buffer 已无强引用(例如 `buffer = null` 或作用域结束)
- 用反射获取 `cleaner` 字段(JDK 8/11 是 `sun.misc.Cleaner`,JDK 14+ 是 `jdk.internal.ref.Cleaner`)
- 调用 `cleaner.clean()`,立即执行 `unsafe.freeMemory()`,不触发 GC、无 STW
- 示例关键代码:需注意模块封装限制(如 JDK 17+ 默认禁止反射访问内部 API,需加 `--add-opens java.base/jdk.internal.ref=ALL-UNNAMED`)
更稳妥的工程实践:复用代替频繁分配
比“手动回收”更根本的解法,是减少 `DirectByteBuffer` 的创建频次:
- 使用 Netty 的 `PooledByteBufAllocator`,缓冲区可回收复用,避免反复申请堆外内存
- 设置 `-Dio.netty.maxDirectMemory=0` 强制走堆内路径(适合低吞吐或调试阶段)
- 监控 JMX 指标 `java.nio:type=BufferPool,name=direct` 的 `MemoryUsed`,超阈值(如 MaxDirectMemorySize 的 70%)及时告警
- JVM 启动参数建议用 `-XX:+ExplicitGCInvokesConcurrent`(G1/ZGC 下安全),而非 `-XX:+DisableExplicitGC`(后者会让 `System.gc()` 完全失效,某些旧框架兜底逻辑会失灵)
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










