有没有在lambda内部解引用变量以允许垃圾回收的逻辑?

huangapple go评论71阅读模式
英文:

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

问题

我有一个占用内存较多的对象(foo),其中包含一个较小的对象(bar)。我希望确保 foo 被垃圾回收,但在程序的生命周期内需要访问 lambda 中的 hello 字段。

我想知道是否有任何逻辑,可以先将 bar 解引用为自己的变量,然后再创建我的 lambda。我认为这样可能允许 foo 被垃圾回收,但不太确定。

也就是说,在以下示例中,因为我只使用了 bar.hello,所以在创建 lambda 前,是否在执行 B 而不是 A 有任何逻辑,以允许 foo 被垃圾回收?第一个 lambda(A)是否隐含地持有对 foo 的引用,而第二个 lambda(B)是否消除了这个引用?

    class Bar { // 在 lambda 中使用
        String hello;
    }
    
    class Foo { // 占用内存较多的类
        Bar bar;
        String world;
    }
    
    Foo foo = new Foo();
    
    // A(通过 foo 访问 bar,foo 是否仍然保留在内存中?)
    run(() -> System.out.println(foo.bar.hello));
    
    // B(foo 不应再有引用,应该被垃圾回收?)
    Bar bar = foo.bar;
    run(() -> System.out.println(bar.hello));
英文:

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));

答案1

得分: 0

适当地说,foo 是一个变量,它引用了一个类 Foo 的实例。

但是,是的,改变 lambda 表达式将使得类 Foo 的实例能够进行垃圾回收。

public class LambdaTest {

    public static class OuterClass {
        public InnerClass inner;
    }

    public static class InnerClass {
        public String value;
    }

    public static void main(String[] args) {
        // 尝试在这里定义 value 打印 lambda 会失败,
        // 因为 'outer1' 既不是 final,也不是 effectively final。
        //
        // 要求 'outer1' 是 final 会启用对 outer 的安全内联。
        // 否则,lambda 会保持对堆栈帧的引用。
        // 将堆栈帧放在堆上的语言不会这样做。Java 不是其中之一。

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

        OuterClass outer1 = new OuterClass();

        // 'outer1' 的值被内联到 lambda 中。
        //
        // 这创建了一个对 'outer1' 值的新引用,这将阻止
        // OuterClass 实例被垃圾回收。

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

        // 对 'outer1' 的新赋值会导致 lambda 定义失败。

        // outer = new OuterClass();

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

        // 虽然 'outer1' 的值被内联,但 'outer.inner.value' 表达式的其他部分没有被内联。
        //
        // 对 'value' 和 'inner' 引用的更改对 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";

        // 'inner2' 引用的 InnerClass 实例没有对 'outer2' 引用的 OuterClass 实例的引用。
        //
        // lambda 'valuePrinter2' 内联了 'inner2' 持有的引用。
        //
        // 对 'valuePrinter2' 的活动引用不会阻止由 'outer2' 引用的值被垃圾回收。

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

    // 输出:
    //
    // Outside lambda1 [ TestValue1 ]
    // Inside lambda1 [ TestValue1 ]
    // Outside lambda1 [ TestValue2 ]
    // Inside lambda1 [ TestValue2 ]
    // Outside lambda1 [ TestValue3 ]
    // Inside lambda1 [ TestValue3 ]
    // Outside lambda2 [ TestValue4 ]
    // Inside lambda2 [ TestValue4 ]
}
英文:

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 ]

}

答案2

得分: 0

以下是翻译好的部分:

这是一个有趣的问题,我个人认为。

以下是我如何稍微简化了代码库:

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 { // 在 lambda 内使用
        String hello = "hello";
    }

    static class Foo { // 占用内存的类
        Bar bar = new Bar();
    }
}

现在,这是一个捕获 lambda,如果你运行它(使用特殊标志:-Djdk.internal.lambda.dumpProxyClasses=/some/path/goes/here),并查看反编译的代码,你会注意到它“捕获”了一个 Foo 实例。用更简单的话说,该 lambda 将在内部转换为一个类(实现了 Runnable),它会将一个 Foo 实例作为参数。

如果你将代码更改为:

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

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

以相同的标志运行它,然后进行反编译 - 你会看到现在捕获了一个 Bar 实例。

这改变了情况。


这意味着当碰到这行时:

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

Foo 实例就有资格进行垃圾回收,这正是你想要的。

另一个代码块:

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

如我所说,会被转换为一个实现了 Runnable 接口的 class,大致会像这样:

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);
     }
}

虽然不完全是这样,但对于这个例子来说,我将其翻译得不完全正确也没有关系。这意味着 Thread 实例内部将会存储一个 Runnable 实例(RunnableImpl 的一个实例),而根据上述所见,RunnableImpl 实例内部将会有一个 Foo 的实例。

因此,在第二种情况下,只有当线程结束时,foo 才能被回收,这可能会比线程启动前的时间要晚得多。

英文:

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.

huangapple
  • 本文由 发表于 2020年9月14日 23:58:57
  • 转载请务必保留本文链接:https://go.coder-hub.com/63887867.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定