Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unreachable objects on the stack cannot be garbage collected

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 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.

like image 360
Robin Avatar asked Aug 03 '17 06:08

Robin


2 Answers

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.

like image 166
Jon Skeet Avatar answered Oct 16 '22 05:10

Jon Skeet


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);
}
like image 26
Adeel Avatar answered Oct 16 '22 04:10

Adeel