java nio 中清理直接缓冲区(direct buffer)的核心机制依赖于 sun.misc.cleaner,它通过注册一个 phantomreference 到 referencequeue 来监听缓冲区的垃圾回收。当堆上的 bytebuffer 对象被 gc 回收时,jvm 会触发 cleaner 的 clean() 方法,该方法最终调用 unsafe 的 freememory() 释放底层操作系统分配的内存。

Cleaner 是如何关联到 ByteBuffer 的?
Cleaner 与 ByteBuffer 的关联是通过 Unsafe.allocateDirect() 创建缓冲区时建立的。在创建过程中,JVM 会实例化一个 Cleaner 对象,并将其作为虚引用包裹住 ByteBuffer 实例。这意味着 Cleaner 并不持有对 ByteBuffer 的强引用,而是依赖 JVM 的垃圾回收机制来通知它何时执行清理任务。

import java.nio.ByteBuffer;
import sun.misc.Cleaner;
public class DirectBufferDemo {
public static void main(String[] args) {
// 分配 1MB 的直接缓冲区
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);
// 获取内部的 Cleaner 实例
Cleaner cleaner = ((java.nio.Bits.Cleaner) buffer).cleaner();
if (cleaner != null) {
System.out.println("Cleaner is registered: " + cleaner.isCleaned());
}
// 显式清理(可选,通常由 GC 触发)
// cleaner.clean();
// 移除强引用,等待 GC
buffer = null;
System.gc();
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
}
上述代码展示了如何获取并检查 Cleaner 的状态。注意,ByteBuffer.allocateDirect() 内部调用了 Bits.reserveMemory() 和 Unsafe.allocateMemory(),并在返回的 ByteBuffer 中嵌入了 Cleaner 引用。当 buffer 变量置为 null 后,若没有其他地方引用该对象,GC 将回收它并触发 Cleaner。
为什么需要 Cleaner 而不是 finalize()?
早期 Java 版本曾使用 finalize() 方法来清理直接内存,但因其不可预测的执行时间和性能开销而被弃用。Cleaner 基于 PhantomReference,能在对象被回收后立即进入 ReferenceQueue,由专门的 ReferenceHandler 线程异步处理。这种方式更轻量且确定性更强,避免了 finalize() 导致的内存泄漏风险。

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.nio.ByteBuffer;
public class CleanerVsFinalize {
public static void main(String[] args) throws Exception {
ReferenceQueue<bytebuffer> queue = new ReferenceQueue();
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
// 创建 PhantomReference 模拟 Cleaner 行为
PhantomReference<bytebuffer> ref = new PhantomReference(buffer, queue);
buffer = null; // 解除强引用
System.gc();
Thread.sleep(500);
// 检查是否进入队列
Object polled = queue.poll();
System.out.println("Reference enqueued: " + (polled != null));
}
}</bytebuffer></bytebuffer>
此示例验证了 PhantomReference 在对象回收后的行为。虽然 Cleaner 是 JDK 内部实现,但其原理与此一致:一旦 ByteBuffer 被回收,Cleaner 会被加入队列,进而触发 clean() 方法释放 native 内存。相比 finalize(),这种机制确保了资源释放的及时性和可靠性。
Cleaner 清理失败的可能原因有哪些?
尽管 Cleaner 设计可靠,但在某些场景下可能出现清理延迟或失败。主要原因包括:ReferenceHandler 线程被阻塞、JVM 堆内存不足导致 GC 停顿过长、或存在其他强引用阻止对象被回收。此外,若手动调用 System.exit() 或发生致命错误,Cleaner 可能无法执行。
import java.nio.ByteBuffer;
public class CleanerFailureCase {
public static void main(String[] args) {
// 保持强引用,阻止 GC
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
// 即使调用 gc(),由于强引用存在,Cleaner 不会触发
System.gc();
try { Thread.sleep(1000); } catch (InterruptedException e) {}
// 此时 buffer 仍有效,内存未释放
System.out.println("Buffer remaining: " + buffer.remaining());
}
}
该代码演示了因强引用存在而导致 Cleaner 无法触发的情况。开发者需确保在不再需要直接缓冲区时,主动解除所有引用,以便 Cleaner 能正常执行清理操作。同时,监控 ReferenceHandler 线程状态也有助于排查潜在的清理延迟问题。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











