Is there any logic to dereferencing a variable for use within a lambda to allow garbage collection?

Viewed 85

I have a memory-heavy object (foo) and a smaller object (bar) held within. I want to make sure foo is garbage collected but need access to the hello field within my lambda for the lifetime of the program.

I am wondering if there is any logic to first dereferencing bar into its own variable before creating my lambda. I believe that this may allow foo to be garbage collected but am not certain.

I.e. in the following example, as I am only using bar.hello, is there any logic in doing B over A to allow foo to be garbage collected? Does the first lambda (A) implicitly hold a reference to foo, and does the second (B) remove this reference?

    class Bar { // Use within lambda
        String hello;
    }
    
    class Foo { // Memory-heavy class
        Bar bar;
        String world;
    }
    
    Foo foo = new Foo();
    
    // A (access bar through foo, will foo remain in memory?)
    run(() -> System.out.println(foo.bar.hello));
    
    // B (foo should have no more references, should be GC'd?)
    Bar bar = foo.bar;
    run(() -> System.out.println(bar.hello));
2 Answers

Properly speaking, foo is a variable, which references a class Foo instance.

But yes, changing the lambda expression will enable garbage collection of the class Foo instance.

public class LambdaTest {

public static class OuterClass {
    public InnerClass inner;
}

public static class InnerClass {
    public String value;
}

public static void main(String[] args) {
    // Attempting to define the value printer lambda here fails,
    // since 'outer1' is neither final nor effectively final.
    //
    // Requiring 'outer1' to be final enables safe inlining of
    // outer's value.  Otherwise, a reference to stack frame
    // would be held by the lambda.  Languages which place stack frames
    // on the heap don't do this.  java is not one of those language.

    // OuterClass outer1 = null;
    //
    // Runnable valuePrinter =
    //     () -> { System.out.println("Value [ " + outer.inner.value + " ]"); };
    // outer1 = new OuterClass();

    OuterClass outer1 = new OuterClass();       

    // The value of 'outer1' is inlined in the lambda.
    //
    // This creates a new reference to the value of 'outer1', which will prevent
    // that OuterClass instance from being garbage collected.

    Runnable valuePrinter1 =
        () -> { System.out.println("Inside lambda1 [ " + outer1.inner.value + " ]"); };

    // A new assignment to 'outer1' would cause the lambda definition to
    // fail.

    // outer = new OuterClass();

    outer1.inner = new InnerClass();
    outer1.inner.value = "TestValue1";
    System.out.println("Outside lambda1 [ " + outer1.inner.value + " ]");
    valuePrinter1.run();

    // While the 'outer1' value was inlined, the other parts of the
    // 'outer.inner.value' expression were not.
    //
    // Changes made to the 'value' and 'inner' references are visible to
    // the lambda.

    outer1.inner.value = "TestValue2";
    System.out.println("Outside lambda1 [ " + outer1.inner.value + " ]");
    valuePrinter1.run();
    
    outer1.inner = new InnerClass();
    outer1.inner.value = "TestValue3";
    System.out.println("Outside lambda1 [ " + outer1.inner.value + " ]");
    valuePrinter1.run();
    
    OuterClass outer2 = new OuterClass();               
    InnerClass inner2 = new InnerClass();
    outer2.inner = inner2;
    inner2.value = "TestValue4";

    // The InnerClass instance referenced by 'inner2' has no reference to
    // the OuterClass instance referenced by 'outer2'.
    //
    // The lambda 'valuePrinter2' inlines the reference held by 'inner2'.
    //
    // A live reference to 'valuePrinter2' will not prevent the value referenced
    // by 'outer2' from being garbage collected.

    System.out.println("Outside lambda2 [ " + inner2.value + " ]");
    Runnable valuePrinter2 =
        () -> { System.out.println("Inside lambda2 [ " + inner2.value + " ]"); };
    valuePrinter2.run();
}

// Output:
//
// Outside lambda1 [ TestValue1 ]
// Inside lambda1 [ TestValue1 ]
// Outside lambda1 [ TestValue2 ]
// Inside lambda1 [ TestValue2 ]
// Outside lambda1 [ TestValue3 ]
// Inside lambda1 [ TestValue3 ]
// Outside lambda2 [ TestValue4 ]
// Inside lambda2 [ TestValue4 ]

}

That is one interesting question, imho.

Here is how I've simplified the code base a bit:

public class DeleteMe {

    public static void main(String[] args) {

        Foo foo = new Foo();

        Thread t = new Thread(() -> System.out.println(foo.bar.hello));
        t.start();

    }

    static class Bar { // Use within lambda
        String hello = "hello";
    }

    static class Foo { // Memory-heavy class
        Bar bar = new Bar();
    }
}

Now this is a capturing lambda and if you run it (with a special flag: -Djdk.internal.lambda.dumpProxyClasses=/some/path/goes/here) and look at the de-compiled code, you will notice that it "captures" a Foo instance. In somehow simpler words, that lambda will be transformed into a class (that implements Runnable) internally that will take a Foo instance as an argument.

If you change that code to:

Foo foo = new Foo();
Bar bar = foo.bar;

Thread t = new Thread(() -> System.out.println(bar.hello));
t.start();

Run that with the same flag, decompile - you will see that a Bar instance is now captured.

This changes things.


This means that Foo instance is eligible for GC, as soon as this line is hit:

run(() -> System.out.println(bar.hello));

And this is exactly what you wanted.

The other code that has :

Thread t = new Thread(() -> System.out.println(foo.bar.hello));

as I said, will be transformed to a class that implements Runnable and will sort of look like this:

class RunnableImpl implements Runnable {
     private final Foo foo;

     public RunnableImpl(Foo foo) {
         this.foo = foo;
     }

     public void run(){
         Bar bar = foo.bar;
         System.out.println(bar.hello);
     }
}

It is not exactly like this, but it does not matter (for this example) if I translate it entirely correct. This means that the Thread instance, will have a Runnable instance stored inside it (an instance of RunnableImpl), which in turn as seen above will have an instance of Foo.

So in the second variant, foo can only be collected when the Thread ends; which potentially can be a lot later in time, then before the thread even starts.

Related