Why should I use Weak/SoftReference with Reference

2019-09-21 11:00发布

As I understand working use case of detecting when object was collected and memory already free for weak/soft references it polling this queue and when reference appear in queue we can be sure that memory free.

WeakReference ref = new WeakReference (new Object()) 

Why cannot I poll ref and check that it became null ?

P.S.

according the link provided in comment:

If the garbage collector discovers an object that is weakly reachable, the following occurs: 1.The WeakReference object's referent field is set to null, thereby making it not refer to the heap object any longer.
2.The heap object that had been referenced by the WeakReference is declared finalizable. 3.When the heap object's finalize() method is run and its memory freed, the WeakReference object is added to its ReferenceQueue, if it exists.

Thus if this article write truth and these steps ordered weakreference becomes null after step but object adds to the queue only on 3th step.

Is it truth?

Is it cause why?

Lets research code:

working canonical example:

public class TestPhantomRefQueue {

    public static void main(String[] args)
            throws InterruptedException {

        Object obj = new Object();
        final ReferenceQueue queue = new ReferenceQueue();

        final WeakReference pRef =
                new WeakReference(obj, queue);

        obj = null;

        new Thread(new Runnable() {
            public void run() {
                try {
                    System.out.println("Awaiting for GC");

                    // This will block till it is GCd
                    Reference prefFromQueue;
                    while (true) {
                        prefFromQueue = queue.remove();
                        if (prefFromQueue != null) {
                            break;
                        }
                    }                
                    System.out.println("Referenced GC'd");
                    System.out.println(pRef.get());


                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();

        // Wait for 2nd thread to start
        Thread.sleep(2000);

        System.out.println("Invoking GC");
        System.gc();
    }
}

this code output:

Awaiting for GC
Invoking GC
Referenced GC'd
null

Ok, I understand why it does work.

Lets change code a bit:

public class TestPhantomRefQueue {

    public static void main(String[] args)
            throws InterruptedException {

        Object obj = new Object();
        final ReferenceQueue queue = new ReferenceQueue();

        final WeakReference pRef =
                new WeakReference(obj, queue);

        obj = null;

        new Thread(new Runnable() {
            public void run() {
                try {
                    System.out.println("Awaiting for GC");

                    while (true) {
                        if (pRef.get() == null) {
                           Thread.sleep(100);
                            break;
                        }
                    }
                    System.out.println("Referenced GC'd");
                    System.out.println(pRef.get());


                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();

        // Wait for 2nd thread to start
        Thread.sleep(2000);

        System.out.println("Invoking GC");
        System.gc();
    }
}

this variant hangs in while loop and output:

Awaiting for GC
Invoking GC

Please explain this behaviour.

3条回答
我只想做你的唯一
2楼-- · 2019-09-21 11:01

With little modification your code would produce anticipated result. See code snippet below.

In your code, line

pRef.get() == null

would assign pRef.get() to temporary slot in frame of method run(). Event after condition is calculated, slot is not clear automatically.

Garbage collector treat all slots/local variable in active frames on stack as GC roots. Unintentionally you are creating strong reference to your object so it wont be cleared.

I modified version, I have moved pRef.get() to nested method. Once execution returns from method its frame is disposed, so to reference to object remains to prevent GC to collect it.

Of cause, if JVM would recompile run() method and inline isRefernceCollected(pRef) call, it may break again.

In summary, reference queues give you deterministic and efficient way to handle reference. Pooling can work but it is fragile and depends of code compilation by javac and JVM JIT.

Modified code snippet.

public class TestPhantomRefQueue {

    public static void main(String[] args)
            throws InterruptedException {

        Object obj = new Object();
        final ReferenceQueue queue = new ReferenceQueue();

        final WeakReference pRef =
                new WeakReference(obj, queue);

        obj = null;

        new Thread(new Runnable() {
            public void run() {
                try {
                    System.out.println("Awaiting for GC");

                    while (true) {
                        if (isRefernceCollected(pRef)) {
                           Thread.sleep(100);
                            break;
                        }
                    }
                    System.out.println("Referenced GC'd");
                    System.out.println(pRef.get());


                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

            protected boolean isRefernceCollected(final WeakReference pRef) {
                return pRef.get() == null;
            }
        }).start();

        // Wait for 2nd thread to start
        Thread.sleep(2000);

        System.out.println("Invoking GC");
        System.gc();
    }
}

Output

Awaiting for GC
Invoking GC
Referenced GC'd
null
查看更多
我欲成王,谁敢阻挡
3楼-- · 2019-09-21 11:10

As long as the original reference is available to any part of your program, it will not be garbage collected. Having access to the original reference in order to check it for being null would prevent the object from being ever freed. That's why you need special reference types to handle this situation.

查看更多
我想做一个坏孩纸
4楼-- · 2019-09-21 11:11

It is not correct that a temporary reference will stay in a stack frame’s slot, like stated in this answer. However, when you poll a variable in a tight loop, an optimizer might create code keeping a reference for several iterations instead of reading the value from the main memory each time. Since System.gc() is only a hint anyway and you are calling it exactly one time, there is no guaranty that this particular gc cycle will find this object to be unreachable, if the cycle ever happened. After that, since there are no subsequent GC cycles, your polling loop will run forever.

You have also misplaced your sleep call. It will sleep after it detected the collection, which makes no sense. If you change

while (true) {
    if (pRef.get() == null) {
       Thread.sleep(100);
        break;
    }
}

to

while (true) {
    if (pRef.get() == null) {
        break;
    }
    Thread.sleep(100);
}

it will work in most environments. But you don’t need such a complicated example:

WeakReference<Object> ref=new WeakReference<>(new Object());
int count=0;
while(ref.get()!=null) {
    System.gc();
    count++;
}
System.out.println("collected after "+count+" polls");

works on most systems. On my system, it will collect the object right on the first cycle.

So what’s the main difference between polling a ReferenceQueue and polling a Reference?

As the name suggests, a ReferenceQueue is a queue, allowing to handle more than one reference. Instead of polling thousands of references one after another to find out whether one of them has a collected referent, you can poll the ReferenceQueue to get the first collected, if there is one. If none of them has been collected, you’re done right after the first poll.

So the polling costs of a reference queue scales with the number of collected objects, not with the total number of existing. A typical example is WeakHashMap. After a key has been collected, the associated entry, which still has a reference to the value, must be removed from the backing array. Instead of iterating over the entire array, checking each entry for presence of the key, only the reference queue has to be polled for collected keys. Since the extended reference object has remembered the hash code, it can be found in the array in O(1) time. This is crucial, as there is no dedicated cleanup method that anyone has to call. As it works like an ordinary map, just with weak key semantics, the polling happens transparently on every method call on that map. So it must be fast.

查看更多
登录 后发表回答