Java 中的 Reference Queue:软/弱引用对象被回收时的通知与应用
大家好,今天我们来深入探讨 Java 中一个重要的概念:Reference Queue(引用队列)。Reference Queue 主要用于在软引用(SoftReference)、弱引用(WeakReference)、幻象引用(PhantomReference)等引用对象被垃圾回收器回收时,接收相应的通知。理解并合理运用 Reference Queue,能帮助我们更好地管理内存,避免内存泄漏,并实现一些高级的内存管理策略。
1. 引用类型回顾:强引用、软引用、弱引用与幻象引用
在深入 Reference Queue 之前,我们先简要回顾一下 Java 中的四种引用类型:
| 引用类型 | 特性 | 
|---|---|
| 强引用 (StrongReference) | 这是最常见的引用类型。只要有强引用指向一个对象,垃圾回收器就永远不会回收该对象。即便 JVM 内存不足,宁愿抛出 OutOfMemoryError 错误,也不会回收强引用指向的对象。 | 
| 软引用 (SoftReference) | 当 JVM 内存足够时,垃圾回收器不会回收软引用指向的对象。只有在 JVM 内存不足,即将抛出 OutOfMemoryError 错误时,才会考虑回收软引用指向的对象。软引用非常适合用于实现内存敏感的缓存。 | 
| 弱引用 (WeakReference) | 只要垃圾回收器发现了弱引用指向的对象,无论当前 JVM 内存是否充足,都会回收该对象。但是,由于垃圾回收器是一个低优先级的线程,因此不一定会很快发现弱引用对象。弱引用常用于实现规范映射 (Canonical Mapping),例如 java.util.WeakHashMap。 | 
| 幻象引用 (PhantomReference) | 幻象引用也称为虚引用。它是一种最弱的引用类型。幻象引用不能通过 get() 方法获取对象实例。它的主要作用是跟踪对象被垃圾回收器回收的活动。当一个对象被垃圾回收器回收时,如果存在指向该对象的幻象引用,那么该幻象引用会被放入与之关联的 Reference Queue 中。幻象引用必须和 Reference Queue 联合使用。常用于跟踪对象的回收状态,在对象被回收后进行一些清理工作,例如释放直接内存。 | 
2. ReferenceQueue 的作用与原理
ReferenceQueue 是一个 FIFO (First-In, First-Out) 队列,用于存储已被垃圾回收器回收的引用对象。当一个软引用、弱引用或幻象引用指向的对象被垃圾回收器回收后,JVM 会将该引用对象放入与之关联的 ReferenceQueue 中(如果该引用对象在创建时指定了 ReferenceQueue)。
工作原理:
- 创建引用对象时关联 ReferenceQueue: 在创建 SoftReference、WeakReference 或 PhantomReference 对象时,可以选择传入一个 ReferenceQueue 对象。这个 ReferenceQueue 对象将用于接收该引用对象被回收时的通知。
 - 垃圾回收器回收对象: 当垃圾回收器发现一个软引用或弱引用对象指向的对象可以被回收时(对于幻象引用,则是已经被回收),它会将该引用对象放入与之关联的 ReferenceQueue 中。
 - 从 ReferenceQueue 中获取通知: 可以通过 ReferenceQueue 的 
poll()或remove()方法来获取已被回收的引用对象。poll()方法是非阻塞的,如果队列为空,则立即返回null。remove()方法是阻塞的,它会一直阻塞,直到队列中有元素可供获取,或者被中断。 - 执行清理操作: 从 ReferenceQueue 中获取到引用对象后,通常需要执行一些清理操作,例如释放资源,清理缓存等。
 
3. ReferenceQueue 的使用示例
下面我们分别通过软引用、弱引用和幻象引用的示例来演示 ReferenceQueue 的使用。
3.1 软引用与 ReferenceQueue
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
public class SoftReferenceExample {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个 ReferenceQueue
        ReferenceQueue<Object> queue = new ReferenceQueue<>();
        // 创建一个对象
        Object obj = new Object();
        // 创建一个指向该对象的软引用,并关联 ReferenceQueue
        SoftReference<Object> softReference = new SoftReference<>(obj, queue);
        // 将 obj 置为 null,断开强引用
        obj = null;
        // 打印软引用对象,确认对象存在
        System.out.println("SoftReference: " + softReference.get());
        // 强制进行垃圾回收
        System.gc();
        Thread.sleep(100); // 等待 GC 执行
        // 从 ReferenceQueue 中获取被回收的软引用
        java.lang.ref.Reference<?> ref = queue.poll();
        if (ref != null) {
            System.out.println("SoftReference 被回收了: " + ref);
        } else {
            System.out.println("SoftReference 尚未被回收.");
        }
        // 再分配大量内存,模拟内存不足的情况
        try {
            byte[] largeArray = new byte[1024 * 1024 * 500]; // 500MB
        } catch (OutOfMemoryError e) {
            System.out.println("OutOfMemoryError occurred.");
        }
        System.gc();
        Thread.sleep(100);
        ref = queue.poll();
        if (ref != null) {
            System.out.println("SoftReference 被回收了 (OOM 后): " + ref);
        } else {
            System.out.println("SoftReference 尚未被回收 (OOM 后).");
        }
    }
}
在这个例子中,我们创建了一个软引用 softReference,它指向一个 Object 对象。我们将 obj 置为 null,断开了强引用。然后,我们手动调用 System.gc() 触发垃圾回收。由于是软引用,在内存充足的情况下,GC不一定会回收这个对象。之后我们模拟分配大量内存,触发 OOM,再次进行GC,此时软引用指向的对象很可能会被回收。如果软引用指向的对象被回收了,JVM 会将 softReference 对象放入 queue 中。我们可以通过 queue.poll() 方法来检查 softReference 是否被回收。
3.2 弱引用与 ReferenceQueue
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
public class WeakReferenceExample {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个 ReferenceQueue
        ReferenceQueue<Object> queue = new ReferenceQueue<>();
        // 创建一个对象
        Object obj = new Object();
        // 创建一个指向该对象的弱引用,并关联 ReferenceQueue
        WeakReference<Object> weakReference = new WeakReference<>(obj, queue);
        // 将 obj 置为 null,断开强引用
        obj = null;
        // 打印弱引用对象,确认对象存在
        System.out.println("WeakReference: " + weakReference.get());
        // 强制进行垃圾回收
        System.gc();
        Thread.sleep(100); // 等待 GC 执行
        // 从 ReferenceQueue 中获取被回收的弱引用
        java.lang.ref.Reference<?> ref = queue.poll();
        if (ref != null) {
            System.out.println("WeakReference 被回收了: " + ref);
        } else {
            System.out.println("WeakReference 尚未被回收.");
        }
    }
}
这个例子与软引用例子类似,只是将 SoftReference 替换为 WeakReference。由于是弱引用,只要垃圾回收器发现了它,就会立即回收它指向的对象,并将其放入 ReferenceQueue 中。
3.3 幻象引用与 ReferenceQueue
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
public class PhantomReferenceExample {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个 ReferenceQueue
        ReferenceQueue<Object> queue = new ReferenceQueue<>();
        // 创建一个对象
        Object obj = new Object();
        // 创建一个指向该对象的幻象引用,并关联 ReferenceQueue
        PhantomReference<Object> phantomReference = new PhantomReference<>(obj, queue);
        // 将 obj 置为 null,断开强引用
        obj = null;
        // 强制进行垃圾回收
        System.gc();
        Thread.sleep(100); // 等待 GC 执行
        // 幻象引用不能通过 get() 方法获取对象实例,总是返回 null
        System.out.println("PhantomReference: " + phantomReference.get()); // 输出 null
        // 从 ReferenceQueue 中获取被回收的幻象引用
        java.lang.ref.Reference<?> ref = queue.poll();
        if (ref != null) {
            System.out.println("PhantomReference 被回收了: " + ref);
        } else {
            System.out.println("PhantomReference 尚未被回收.");
        }
    }
}
在这个例子中,我们创建了一个幻象引用 phantomReference,它指向一个 Object 对象。请注意,幻象引用不能通过 get() 方法获取对象实例,总是返回 null。当对象被垃圾回收器回收后,JVM 会将 phantomReference 对象放入 queue 中。幻象引用主要用于跟踪对象的回收状态,并在对象被回收后进行一些清理工作。
4. ReferenceQueue 的应用场景
ReferenceQueue 在实际开发中有很多应用场景,下面列举一些常见的例子:
- 资源清理: 当使用软引用或弱引用来缓存资源时,可以使用 ReferenceQueue 来跟踪资源的回收情况。当资源被回收后,可以从 ReferenceQueue 中获取通知,并释放相应的系统资源,例如关闭文件流,释放网络连接等。
 - 缓存管理: 可以使用软引用或弱引用来构建内存敏感的缓存。当缓存中的对象被回收后,可以通过 ReferenceQueue 来更新缓存,避免使用已经失效的对象。
 - 直接内存管理:  在使用 
ByteBuffer.allocateDirect()分配直接内存时,直接内存的释放需要通过sun.misc.Cleaner对象来完成。Cleaner对象通常使用幻象引用来跟踪直接内存的回收情况。当直接内存被回收后,Cleaner对象会将释放直接内存的请求提交给一个后台线程来执行。 - 对象池: 可以使用 ReferenceQueue 来跟踪对象池中对象的回收情况。当对象被回收后,可以从 ReferenceQueue 中获取通知,并将对象重新添加到对象池中,以便下次使用。
 
5. 深入理解 ReferenceQueue 的实现细节
ReferenceQueue 的实现依赖于 JVM 的垃圾回收机制。当垃圾回收器发现一个引用对象指向的对象可以被回收时,它会将该引用对象放入与之关联的 ReferenceQueue 中。这个过程是由 JVM 自动完成的,不需要我们手动干预。
ReferenceQueue 内部使用一个链表来存储待处理的引用对象。poll() 和 remove() 方法从链表中获取引用对象。remove() 方法会阻塞,直到链表中有元素可供获取,或者被中断。
6. 避免内存泄漏
不正确地使用引用和 ReferenceQueue 可能会导致内存泄漏。例如,如果创建了一个引用对象,并将其关联到一个 ReferenceQueue,但是没有及时从 ReferenceQueue 中获取该引用对象,那么该引用对象将一直存在于 ReferenceQueue 中,导致内存泄漏。
以下是一些避免内存泄漏的建议:
- 及时处理 ReferenceQueue 中的引用对象: 应该创建一个专门的线程来处理 ReferenceQueue 中的引用对象,并及时执行清理操作。
 - 避免强引用指向引用对象: 避免在清理线程中创建强引用指向引用对象,因为这会阻止垃圾回收器回收引用对象。
 - 使用正确的引用类型: 根据实际需求选择合适的引用类型。如果只需要在内存不足时才回收对象,可以使用软引用。如果希望对象尽快被回收,可以使用弱引用。如果只是想跟踪对象的回收状态,可以使用幻象引用。
 
7. 代码示例:使用 ReferenceQueue 进行资源清理
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
public class ResourceCleaner {
    private static final ReferenceQueue<Object> queue = new ReferenceQueue<>();
    private static final CleanerThread cleanerThread = new CleanerThread();
    static {
        cleanerThread.setDaemon(true); // 设置为守护线程
        cleanerThread.start();
    }
    public static void register(Object resource, Runnable cleanupAction) {
        new ResourcePhantomReference(resource, queue, cleanupAction);
    }
    private static class ResourcePhantomReference extends PhantomReference<Object> {
        private final Runnable cleanupAction;
        ResourcePhantomReference(Object referent, ReferenceQueue<? super Object> q, Runnable cleanupAction) {
            super(referent, q);
            this.cleanupAction = cleanupAction;
        }
    }
    private static class CleanerThread extends Thread {
        @Override
        public void run() {
            while (true) {
                try {
                    Reference<?> ref = queue.remove();
                    ResourcePhantomReference resourceRef = (ResourcePhantomReference) ref;
                    resourceRef.cleanupAction.run();
                    resourceRef.clear(); // 清除引用,避免内存泄漏
                } catch (InterruptedException e) {
                    // Handle interruption (e.g., during shutdown)
                    break; // Exit the loop
                }
            }
        }
    }
    public static void main(String[] args) throws IOException {
        // 模拟一个需要清理的资源 (InputStream)
        InputStream inputStream = null;
        try {
            inputStream = ResourceCleaner.class.getClassLoader().getResourceAsStream("test.txt"); // 假设存在 test.txt 文件
            if (inputStream == null) {
                System.err.println("test.txt not found.");
                return;
            }
            // 注册资源和清理操作
            final InputStream finalInputStream = inputStream;
            ResourceCleaner.register(new Object(), () -> {
                try {
                    System.out.println("Cleaning up InputStream...");
                    finalInputStream.close();
                } catch (IOException e) {
                    System.err.println("Error closing InputStream: " + e.getMessage());
                }
            });
            // 使用资源
            System.out.println("Reading from InputStream...");
            int data = inputStream.read();
            while (data != -1) {
                System.out.print((char) data);
                data = inputStream.read();
            }
            System.out.println();
            // 将 inputStream 置为 null,触发垃圾回收
            inputStream = null;
            System.gc();
            Thread.sleep(1000); // 等待 GC 执行和清理线程运行
        } finally {
            // 确保即使出现异常,资源也能被注册清理。如果inputStream一开始就为null,则不进行后续操作,避免空指针异常
            if(inputStream != null){
                final InputStream finalInputStream = inputStream;
                ResourceCleaner.register(new Object(), () -> {
                    try {
                        System.out.println("Cleaning up InputStream... (finally block)");
                        finalInputStream.close();
                    } catch (IOException e) {
                        System.err.println("Error closing InputStream (finally block): " + e.getMessage());
                    }
                });
                System.out.println("Releasing InputStream in finally block...");
                inputStream = null;
                System.gc();
                Thread.sleep(1000);
            }
        }
    }
}
在这个例子中,我们创建了一个 ResourceCleaner 类,它使用幻象引用和 ReferenceQueue 来跟踪资源的回收情况。register() 方法用于注册资源和清理操作。当资源被回收后,CleanerThread 线程会从 ReferenceQueue 中获取通知,并执行清理操作,例如关闭 InputStream。  这个例子展示了如何使用 ReferenceQueue 来确保资源在不再使用时被正确释放,避免资源泄漏。
8. 选择合适的引用类型和 ReferenceQueue
选择哪种引用类型和是否使用 ReferenceQueue 取决于具体的应用场景。
- 强引用: 适用于必须保持存活的对象。
 - 软引用: 适用于内存敏感的缓存。
 - 弱引用: 适用于规范映射等场景。
 - 幻象引用: 适用于跟踪对象的回收状态,并在对象被回收后进行一些清理工作。
 
如果需要跟踪对象的回收情况,并执行一些清理操作,那么应该使用 ReferenceQueue。
ReferenceQueue 的核心价值
ReferenceQueue 提供了一种机制,让我们能够在对象被垃圾回收器回收时收到通知,从而执行一些必要的清理工作。这对于资源管理、缓存管理和直接内存管理等场景非常有用。
ReferenceQueue 辅助内存管理
ReferenceQueue 配合软引用、弱引用和幻象引用,可以构建更加智能的内存管理策略,提高应用程序的性能和稳定性。
总结与实践建议
- 理解 Java 中四种引用类型的特性,根据实际需求选择合适的引用类型。
 - 使用 ReferenceQueue 来跟踪软引用、弱引用和幻象引用对象的回收情况。
 - 创建一个专门的线程来处理 ReferenceQueue 中的引用对象,并及时执行清理操作。
 - 避免强引用指向引用对象,防止内存泄漏。
 - 在资源管理、缓存管理和直接内存管理等场景中,合理运用 ReferenceQueue,可以提高应用程序的性能和稳定性。
 
希望今天的分享能够帮助大家更好地理解和使用 ReferenceQueue。谢谢大家!