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:
- From what I understood from that byte code, I would have expected that
object1is removed from the stack (overwritten byobject2), and only the last object accessed in the loop (object2) would not be GC-ed. Changing the
forloop tofor (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.