Against my expectations, the following program
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.List;
public class StackTest {
public static void main(String[] args) {
Object object1 = new Object();
Object object2 = new Object();
List<Object> objects = Arrays.asList(object1, object2);
WeakReference<Object> ref1 = new WeakReference<>(object1);
WeakReference<Object> ref2 = new WeakReference<>(object2);
for (Object o : objects) {
System.out.println(o);
}
objects = null;
object1 = null;
object2 = null;
System.gc();
System.gc();
System.gc();
System.out.println("ref1: " + ref1.get());
System.out.println("ref2: " + ref2.get());
}
}
still prints out
ref1: java.lang.Object@15db9742
ref2: java.lang.Object@6d06d69c
meaning object1
and object2
are not GC-ed.
However, when removing the for
loop from the program, those objects can be GC-ed and the program prints.
ref1: null
ref2: null
Moving the for
loop to a separate method has the same effect: the objects are GC-ed at the end of the program.
What I suspect is happening is that that for
loop stores those objects on the stack, and does not remove them afterwards. And since the object is still present on the stack, it cannot be GC-ed.
Looking at the byte code (which I am really not that good at) seems to support this hypothesis:
53: invokeinterface #6, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
58: astore 6
60: aload 6
62: invokeinterface #7, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
67: ifeq 90
70: aload 6
72: invokeinterface #8, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
77: astore 7
79: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
82: aload 7
84: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
87: goto 60
I see astore
commands, but I couldn't spot a place in the byte code where those are removed from the stack again.
However, I have two issues with my theory:
object1
is removed from the stack (overwritten by object2
), and only the last object accessed in the loop (object2
) would not be GC-ed.Changing the for
loop to
for (Object o : objects) {
System.out.println(o);
o = null;
}
doesn't change the output of the program. I would have thought that that would clear that reference to the object on the stack.
Question: anybody has a solid theory on why that for
-loop ensures that those objects cannot be GC-ed ? My theory has a few gaping holes in them.
Context:
This problem was encountered in a unit tests which we use to detect memory leaks, based on the Netbeans method NBTestCase#assertGC
. This assertGC
method will fail when an object is still referenced on the heap or on the stack.
In our test, we have code like
@Test
public void test(){
List<DisposableFoo> foos = ...;
doStuffWithFoo(foos);
List<WeakReference<DisposableFoo>> refs = ...;
for(DisposableFoo foo : foos){
disposeFoo(foo);
}
foos = null;
assertGC(refs);
}
which kept on failing, until we removed the for
-loop.
We already have a workaround (move the for
-loop to a separate method), but I would like to understand why our original code does not work.
The problem is that you still have a list iterator on the stack, and that list iterator has a reference to the original list. That's keeping the list alive just as if you'd never set objects
to null.
An iterator has to keep a reference to the original collection, so that it can request the next item etc. For a regular Iterator<E>
it could potentially set its internal reference to null
once hasNext()
had returned false, but for a list iterator even that's not the case, as you can move in both directions within a list iterator.
Change your for loop and then try again
for (int i = 0; i < objects.size(); i++)
{
Object o = objects.get(i);
System.out.println(o);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With