How to use PhantomReference as finalize() Replacement
Asked Answered
G

2

9

Javadoc 8 for PhantomReference states:

Phantom references are most often used for scheduling pre-mortem cleanup actions in a more flexible way than is possible with the Java finalization mechanism.

So I tried creating a thread that is calling the close() method of a Test Object that is eligible for garbage collection. The run() tries to get all Test Objects pre-mortem.

Actually the retrieved Test Objects are all null. The expected behavior is, that the Test Objects are retrieved and the closemethod is called.

No matter how many Test Objects you create there is not a single Test Object that could be caught pre-mortem (You have to increase the timeouts and call GC multiple times).

What am I doing wrong? Is this a Java Bug?

Runnable Test Code:

I tried to create a Minimal, Complete, and Verifiable example, but it's still quite long. I use java version "1.8.0_121" 32-bit on Windows 7 64-bit.

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");
            }
        }
    }
}

Expected Output:

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

Actual Output:

System.gc()
Test Obj is null
Test Obj is null
Thread Interrupted
Gog answered 9/4, 2017 at 20:54 Comment(2)
Test testObj = mReferenceQueue.remove().get(); will always be null . Change that block of code to mReferenceQueue.remove().close() and it'll work. queue.remove() will block correctly and always return you an object. There's no need to test for null.Godavari
Hello @Godavari . mReferenceQueue.remove() will return a Reference<? extends Test> Object and not the Test Object, so I can't call mReferenceQueue.remove().close(). Maybe you can provide more details.Gog
P
8

This is by design. Unlike finalize(), which makes an object reachable again, objects referable by a Reference object only can not be made reachable again. So when you are going to manage a resource through it, you have to store the necessary information into another object. It’s not unusual, to use the Reference object itself for it.

Consider the following modifications to your test program:

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");
            }
        }
    }
}

which will print:

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

showing how using the correct idiom ensures that resources are closed timely and, unlike finalize(), the object can opt out the post-mortem cleanup which makes using the correct idiom even more efficient, as in that case, no additional GC cycle is needed to reclaim the object after finalization.

Patience answered 27/4, 2017 at 13:55 Comment(7)
Could you please explain why the test objects in the try-with-resources block won't be added to the reference queue? Aren't they finalized because of any special relationship between autocloseable and the gc I do not know about?Multiversity
@Multiversity mind the package documentation: “The relationship between a registered reference object and its queue is one-sided. That is, a queue does not keep track of the references that are registered with it. If a registered reference becomes unreachable itself, then it will never be enqueued.” That’s why this solution keeps track with the Set<TestResource> mPhantomStack. The close() method removes the resource, making it unreachable, thus allowing it to get collected.Patience
Invoking clear​() on the reference would also have the effect of preventing enqueuing, but it has to be removed from the global set anyway. — Note that the logic of this solution is the same as implemented in the Cleaner API, which was introduced with Java 9, a few months after this answer. In that API, your close() method would invoke Cleanable.clean() to perform the cleanup action and unregister it.Patience
"The close() method removes the resource, making it unreachable, thus allowing it to get collected." That was my thought, but if I comment out thread.remove(resource); in the close()-method the objects with id 2 and 3 are still not enqueued, even though both references are still held in the stack. You can tryit for yourself.Multiversity
@Multiversity that’s the unreliability of garbage collection. In this specific case, there are dangling references in the stack frame at the point, System.gc() is called. You have two options, you can 1) run the example with -Xcomp or 2) insert, e.g. long dummy = 42; right before the System.gc(); line. In both cases, it will show that thread.remove(resource); makes a difference. In real life applications, you don’t have such issues.Patience
Interesting, didn't no dangling references were possible in java and that you can work around it with this special command line option. Many thanks for your feedback, learned a lot.Multiversity
@Multiversity the point to understand is, that the scope of local variables is a compile-time thing. In the bytecode, there are only write and read instructions addressing indices in the stack frame. So there could be dangling references, but it can also work the other way round; in optimized executions, variables might be still “in scope”, but the referenced object gets collected, because its not used afterwards. -Xcomp just disables the interpreter and the JIT compiler emits code dropping the references right after the last use. As said, in real life applications, this normally doesn’t matterPatience
A
5

get() method on phantom references always return null.

At the moment phantom reference is enqueued object it was referencing is already collected by GC. You need to store data required to clean up in separate object (e.g. you can subclass PhantomReference).

Here you can find example code and more elaborate description about using PhantomReferences.

Unlike finalizer, phantom reference cannot resurrect unreachable object. This is its main advantage, though cost is more complicated supporting code.

Angiosperm answered 9/4, 2017 at 22:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.