【问题标题】:How to use PhantomReference as finalize() Replacement如何使用 PhantomReference 作为 finalize() 替换
【发布时间】:2017-09-04 19:51:55
【问题描述】:

PhantomReference 的 Javadoc 8 状态:

幻影引用最常用于以比 Java 终结机制更灵活的方式调度 pre-mortem 清理操作。

所以我尝试创建一个线程,该线程调用符合垃圾回收条件的测试对象的close() 方法。 run() 尝试获取所有测试对象pre-mortem

实际上检索到的测试对象都是null。预期的行为是检索测试对象并调用close方法。

无论您创建多少个测试对象,都没有一个可以被捕获的测试对象事前分析(您必须增加超时并多次调用 GC)。

我做错了什么?这是 Java 错误吗?

可运行的测试代码:

我尝试创建一个最小、完整且可验证的示例,但它仍然很长。我在 Windows 7 64 位上使用 java version "1.8.0_121" 32 位。

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(); 将始终为 null 。将该代码块更改为mReferenceQueue.remove().close(),它将起作用。 queue.remove() 将正确阻止并始终返回一个对象。无需测试null
  • 你好@Pacerier。 mReferenceQueue.remove() 将返回 Reference&lt;? extends Test&gt; 对象而不是 Test 对象,所以我不能调用 mReferenceQueue.remove().close()。也许您可以提供更多详细信息。

标签: java garbage-collection finalizer finalize phantom-reference


【解决方案1】:

这是设计使然。与使对象再次可访问的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 mind the package documentation: “注册的引用对象与其队列之间的关系是片面的。也就是说,队列不会跟踪向其注册的引用。如果已注册的引用本身变得无法访问,那么它将永远不会入队。”这就是该解决方案与 Set&lt;TestResource&gt; mPhantomStack 保持联系的原因。 close() 方法会删除资源,使其无法访问,从而允许它被收集。
  • 在引用上调用clear​() 也会产生阻止入队的效果,但无论如何都必须从全局集中删除它。 — 请注意,此解决方案的逻辑与the Cleaner API 中实现的逻辑相同,后者是在此答案几个月后随 Java 9 引入的。在该 API 中,您的 close() 方法将调用 Cleanable.clean() 来执行清理操作并取消注册。
  • “close() 方法删除资源,使其无法访问,从而允许它被收集。” 这是我的想法,但如果我注释掉 thread.remove(resource);close()-方法中,id 为 2 和 3 的对象仍未入队,即使两个引用仍保存在堆栈中。你可以自己试试。
  • @user35934 这就是垃圾回收的不可靠性。在这种特定情况下,堆栈帧中存在悬空引用,System.gc() 被调用。您有两个选择,您可以 1) 使用 -Xcomp 运行示例或 2) 插入,例如long dummy = 42; 就在 System.gc(); 行之前。在这两种情况下,它都会显示thread.remove(resource); 有所作为。在现实生活中的应用程序中,您不会遇到此类问题。
【解决方案2】:

get() 幻像引用上的方法总是返回 null。

当幻影引用入队时,它所引用的对象已被 GC 收集。您需要将清理所需的数据存储在单独的对象中(例如,您可以继承 PhantomReference)。

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

与终结器不同,幻像引用不能复活无法访问的对象。这是它的主要优势,虽然成本是更复杂的支持代码。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-10
    • 2020-03-20
    • 2020-12-30
    • 1970-01-01
    • 2023-03-19
    相关资源
    最近更新 更多