欢迎光临
我们一直在努力

如何通过虚幻引用 PhantomReference 监控 Java 堆外内存的回收时机

在 Java 编程中,当我们使用如 ByteBuffer.allocateDirect() 这样的 API 来分配堆外(Off-Heap)内存时,这些资源不受 Java 垃圾收集器(GC)的直接管理。虽然持有堆外内存的 Java 对象本身会被 GC 回收,但我们需要一个机制来确保在 Java 对象被回收的同一时刻,关联的堆外内存也能被安全释放。

依赖于不稳定的 Object.finalize() 方法早已被弃用,因为 finalize() 无法保证执行时间,甚至可能不会执行。解决这一问题的标准且可靠的方法是使用 Java 的虚幻引用 (PhantomReference) 配合 引用队列 (ReferenceQueue)

虚幻引用 (PhantomReference) 的工作原理

虚幻引用是 Java 四种引用类型中最弱的一种。它的主要特点是:

  1. 无法通过 **get() 方法获取到对象实例get()** 总是返回 null)。
  2. 对象只有在被 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() 方法。

总结

通过结合 PhantomReferenceReferenceQueue,我们建立了一个可靠且非阻塞的机制,用于监控 Java 对象被 GC 回收的时机。这对于管理 Java 堆外内存、文件句柄或网络连接等关键外部资源,提供了一个比传统 finalize() 强大得多的解决方案。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » 如何通过虚幻引用 PhantomReference 监控 Java 堆外内存的回收时机
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址