如何使用虚引用作为finalize()的替代方法

9
Javadoc 8中的PhantomReference表示:
幻影引用通常用于以比Java终结机制更灵活的方式安排临终清理操作。
因此,我尝试创建一个调用Test对象的close()方法的线程,该对象符合垃圾回收的条件。run()方法试图获取所有的Test对象,在临终之前就被抓住了。
实际上,检索到的所有Test对象都为null。期望的行为是检索到Test对象并调用close()方法。
无论您创建多少个Test对象,都没有一个可以被抓住在临终之前(您必须增加超时时间并多次调用GC)。
我做错了什么?这是Java的错误吗?
可运行的测试代码:
我试图创建一个最小、完整和可验证的示例,但它仍然相当长。我在Windows 7 64位上使用32位的java版本“1.8.0_121”。
public class TestPhantomReference {

    public static void main(String[] args) throws InterruptedException {
        // Create AutoClose Thread and start it
        AutoCloseThread thread = new AutoCloseThread();
        thread.start();

        // Add 10 Test Objects to the AutoClose Thread
        // Test Objects are directly eligible for GC
        for (int i = 0; i < 2; i++) {
            thread.addObject(new Test());
        }

        // Sleep 1 Second, run GC, sleep 1 Second, interrupt AutoCLose Thread
        Thread.sleep(1000);
        System.out.println("System.gc()");
        System.gc();
        Thread.sleep(1000);
        thread.interrupt();
    }

    public static class Test {
        public void close() {
            System.out.println("close()");
        }
    }

    public static class AutoCloseThread extends Thread {
        private ReferenceQueue<Test> mReferenceQueue = new ReferenceQueue<>();
        private Stack<PhantomReference<Test>> mPhantomStack = new Stack<>();

        public void addObject(Test pTest) {
            // Create PhantomReference for Test Object with Reference Queue, add Reference to Stack
            mPhantomStack.push(new PhantomReference<Test>(pTest, mReferenceQueue));
        }

        @Override
        public void run() {
            try {
                while (true) {
                    // Get PhantomReference from ReferenceQueue and get the Test Object inside
                    Test testObj = mReferenceQueue.remove().get();
                    if (null != testObj) {
                        System.out.println("Test Obj call close()");
                        testObj.close();
                    } else {
                        System.out.println("Test Obj is null");
                    }
                }
            } catch (InterruptedException e) {
                System.out.println("Thread Interrupted");
            }
        }
    }
}

期望输出:

System.gc()
Test Obj call close()
close()
Test Obj call close()
close()
Thread Interrupted

实际输出:

System.gc()
Test Obj is null
Test Obj is null
Thread Interrupted

将代码块 Test testObj = mReferenceQueue.remove().get(); 改为 mReferenceQueue.remove().close(),就可以解决一直返回 null 的问题。queue.remove() 会正确阻塞并始终返回一个对象,因此无需测试是否为 null - Pacerier
你好 @Pacerier。mReferenceQueue.remove() 将返回一个 Reference<? extends Test> 对象而不是 Test 对象,所以我无法调用 mReferenceQueue.remove().close()。也许你可以提供更多细节。 - notes-jj
2个回答

8
这是有意为之的。与使对象再次可达的finalize()不同,由Reference对象引用的对象只能被标记为不可再次可达。因此,当您要通过它来管理资源时,必须将必要的信息存储到另一个对象中。通常可以使用Reference对象本身来实现这一点。
请考虑对测试程序的以下修改:
public class TestPhantomReference {

    public static void main(String[] args) throws InterruptedException {
        // create two Test Objects without closing them
        for (int i = 0; i < 2; i++) {
            new Test(i);
        }
        // create two Test Objects with proper resource management
        try(Test t2=new Test(2); Test t3=new Test(3)) {
            System.out.println("using Test 2 and 3");
        }

        // Sleep 1 Second, run GC, sleep 1 Second
        Thread.sleep(1000);
        System.out.println("System.gc()");
        System.gc();
        Thread.sleep(1000);
    }

    static class TestResource extends PhantomReference<Test> {
        private int id;
        private TestResource(int id, Test referent, ReferenceQueue<Test> queue) {
            super(referent, queue);
            this.id = id;
        }
        private void close() {
            System.out.println("closed "+id);
        }
    }    
    public static class Test implements AutoCloseable {
        static AutoCloseThread thread = new AutoCloseThread();
        static { thread.start(); }
        private final TestResource resource;
        Test(int id) {
            resource = thread.addObject(this, id);
        }
        public void close() {
            resource.close();
            thread.remove(resource);
        }
    }

    public static class AutoCloseThread extends Thread {
        private ReferenceQueue<Test> mReferenceQueue = new ReferenceQueue<>();
        private Set<TestResource> mPhantomStack = new HashSet<>();

        public AutoCloseThread() {
            setDaemon(true);
        }
        TestResource addObject(Test pTest, int id) {
            final TestResource rs = new TestResource(id, pTest, mReferenceQueue);
            mPhantomStack.add(rs);
            return rs;
        }
        void remove(TestResource rs) {
            mPhantomStack.remove(rs);
        }

        @Override
        public void run() {
            try {
                while (true) {
                    TestResource rs = (TestResource)mReferenceQueue.remove();
                    System.out.println(rs.id+" not properly closed, doing it now");
                    mPhantomStack.remove(rs);
                    rs.close();
                }
            } catch (InterruptedException e) {
                System.out.println("Thread Interrupted");
            }
        }
    }
}

这将打印:

using Test 2 and 3
closed 3
closed 2
System.gc()
0 not properly closed, doing it now
closed 0
1 not properly closed, doing it now
closed 1

展示如何使用正确的成语确保资源及时关闭,与finalize()不同的是,对象可以选择退出事后清理,这使得使用正确的成语更加高效,因为在这种情况下,在最终化后不需要额外的GC周期来回收对象。


请问能否解释一下为什么try-with-resources块中的测试对象不会被添加到引用队列中?它们不是因为autocloseable和gc之间的特殊关系而被终结了吗? - user35934
1
请注意 包文档 中的说明:“已注册引用对象与其队列之间的关系是单向的。也就是说,队列不会跟踪已注册的引用。如果已注册的引用本身变得不可达,则它将永远不会被排队。”这就是为什么此解决方案使用 Set<TestResource> mPhantomStack 跟踪资源的原因。close() 方法会删除资源,使其变得不可达,从而允许其被收集。 - Holger
1
在引用上调用 clear() 方法也会防止入队,但它必须从全局集合中删除。请注意,此解决方案的逻辑与 Cleaner API 中实现的逻辑相同,该 API 在 Java 9 中引入,比本答案晚几个月。在该 API 中,您的 close() 方法将调用 Cleanable.clean() 来执行清理操作并注销它。 - Holger
1
close()方法会移除资源,使其无法访问,从而允许对其进行收集。这是我的想法,但如果我在close()方法中注释掉thread.remove(resource);,那么具有id 2和3的对象仍然没有被入队,尽管两个引用仍然存在于堆栈中。你可以自己试试。 - user35934
1
@user35934 这就是垃圾回收的不可靠性。在这种特定情况下,在调用System.gc()时,堆栈帧中存在悬空引用。您有两个选择,可以1)使用-Xcomp运行示例,或者2)在System.gc();行之前插入例如long dummy = 42;。在这两种情况下,它都将显示thread.remove(resource);的差异。在实际应用中,您不会遇到这样的问题。 - Holger
显示剩余2条评论

5

get() 方法在虚引用上总是返回 null。

此时,当虚引用入队的对象已被 GC 回收,它所引用的对象也不复存在。你需要将清理所需的数据存储在单独的对象中(例如,你可以子类化 PhantomReference)。

这里 你可以找到示例代码和关于使用 PhantomReference 的更详细描述。

与 finalizer 不同,虚引用不能使无法访问的对象恢复。这是其主要优点,尽管成本更高,需要更复杂的支持代码。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接