在 Java 编程中,当我们使用如 ByteBuffer.allocateDirect() 这样的 API 来分配堆外(Off-Heap)内存时,这些资源不受 Java 垃圾收集器(GC)的直接管理。虽然持有堆外内存的 Java 对象本身会被 GC 回收,但我们需要一个机制来确保在 Java 对象被回收的同一时刻,关联的堆外内存也能被安全释放。
依赖于不稳定的 Object.finalize() 方法早已被弃用,因为 finalize() 无法保证执行时间,甚至可能不会执行。解决这一问题的标准且可靠的方法是使用 Java 的虚幻引用 (PhantomReference) 配合 引用队列 (ReferenceQueue)。
虚幻引用 (PhantomReference) 的工作原理
虚幻引用是 Java 四种引用类型中最弱的一种。它的主要特点是:
- 无法通过 **get() 方法获取到对象实例(get()** 总是返回 null)。
- 对象只有在被 GC 认定为“即将被回收”(即没有任何强、软、弱引用指向它)时,虚幻引用才会被放入其关联的 ReferenceQueue。
这个特性使得虚幻引用成为回收通知机制的最佳选择。当引用被放入队列时,我们知道 GC 已经完成了对对象的标记工作,并且对象很快就会被清理。此时是执行外部资源清理操作的完美时机。
实战:监控并清理堆外内存
我们创建一个模拟堆外内存的资源类,并实现一个 ResourceCleaner,它继承自 PhantomReference,并在后台线程中监听引用队列。
步骤一:定义资源与清理器
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
// 1. 模拟持有堆外内存的资源对象
class OffHeapResource {
private final long nativeAddress;
private final int size;
public OffHeapResource(int size) {
this.size = size;
// 模拟分配 native 内存
this.nativeAddress = System.nanoTime();
System.out.println("[Allocation] Resource allocated: " + size + " bytes at address " + nativeAddress);
}
// 真正的清理逻辑
public void releaseNativeMemory() {
System.out.println("*** CLEANUP TRIGGERED ***\n[Deallocation] NATIVE MEMORY CLEANED UP: " + size + " bytes at address " + nativeAddress);
// 实际应用中:调用 JNI/Unsafe API 释放 native 内存
}
}
// 2. 虚幻引用清理器
class ResourceCleaner extends PhantomReference<OffHeapResource> implements Runnable {
// 保持对资源的强引用,以便在回收前可以访问其清理方法
private final OffHeapResource resource;
private static final ReferenceQueue<OffHeapResource> queue = new ReferenceQueue<>();
private static Thread cleanerThread;
public ResourceCleaner(OffHeapResource referent) {
// 注册虚幻引用到队列
super(referent, queue);
this.resource = referent;
// 确保后台清理线程启动
if (cleanerThread == null) {
cleanerThread = new Thread(this, "PhantomReference-Monitor");
// 设为守护线程,应用退出时自动关闭
cleanerThread.setDaemon(true);
cleanerThread.start();
System.out.println("[Monitor] Cleaner thread started.");
}
}
@Override
public void run() {
// 持续阻塞并等待引用进入队列
while (true) {
try {
// 阻塞等待,直到有引用被 GC 放入队列
Reference<?> ref = queue.remove();
if (ref instanceof ResourceCleaner) {
ResourceCleaner cleaner = (ResourceCleaner) ref;
// 执行清理操作
cleaner.resource.releaseNativeMemory();
// 清理完成后,手动清除引用,允许 GC 彻底回收该引用对象本身
ref.clear();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
}
步骤二:运行测试
在主程序中,我们创建资源,注册清理器,然后将对资源的强引用设置为 null,触发 GC。
public class PhantomReferenceDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("--- Start Demo ---");
// 1. 创建资源对象
OffHeapResource resource = new OffHeapResource(4096);
// 2. 注册清理器(创建 PhantomReference 并启动监控)
new ResourceCleaner(resource);
// 3. 移除对资源的强引用,使其可被 GC 回收
resource = null;
System.out.println("--------------------------------");
System.out.println("[GC Trigger] Resource reference set to null. Waiting for GC...");
// 4. 强制触发 GC
System.gc();
// 留出时间让后台清理线程处理队列
Thread.sleep(1000);
System.out.println("--- End Demo ---");
}
}
运行结果示例
执行上述代码后,输出结果将清晰地显示堆外内存清理的时机:
--- Start Demo ---
[Allocation] Resource allocated: 4096 bytes at address 1719917525380599000
[Monitor] Cleaner thread started.
--------------------------------
[GC Trigger] Resource reference set to null. Waiting for GC...
*** CLEANUP TRIGGERED ***
[Deallocation] NATIVE MEMORY CLEANED UP: 4096 bytes at address 1719917525380599000
--- End Demo ---
可以看到,在我们将 resource 设置为 null 并调用 System.gc() 之后,ResourceCleaner 接收到通知并精确地执行了 releaseNativeMemory() 方法。
总结
通过结合 PhantomReference 和 ReferenceQueue,我们建立了一个可靠且非阻塞的机制,用于监控 Java 对象被 GC 回收的时机。这对于管理 Java 堆外内存、文件句柄或网络连接等关键外部资源,提供了一个比传统 finalize() 强大得多的解决方案。
汤不热吧