0

If a lambda expression does not refer to any methods or fields of the surrounding instance, does the language guarantee that it doesn't hold a reference to this?

In particular, I want to use lambda expressions to implement java.lang.ref.Cleaner actions. For example:

import static some.Global.cleaner;

public class HoldsSomeResource {
    private final Resource res;
    private final Cleanable cleanup;

    public HoldsSomeResource(Resource res) {
        this.res = res;
        cleanup = cleaner.register(this, () -> res.discard());
    }

    public void discard() {
        cleanup.clean();
    }
}

Clearly, it would be bad if the lambda expression implementing the cleanup action were to hold a reference to this, since it would then never become unreachable. It seems to work when I test it right now, but I can't find the obvious reference in the JLS that it is guaranteed to be safe, so I'm slightly worried that I might run into problems in alternative and/or future Java implementations.

Dolda2000
  • 25,216
  • 4
  • 51
  • 92

2 Answers2

3

The specification does indeed not mention this behavior, but there is a statement in this document from Brian Goetz:

References to this — including implicit references through unqualified field references or method invocations — are, essentially, references to a final local variable. Lambda bodies that contain such references capture the appropriate instance of this. In other cases, no reference to this is retained by the object.

While this isn’t the official specification, Brian Goetz is the most authoritative person we can have to make such a statement.

This behavior of lambda expressions is as intentional as it can be. The cited text continues with

This has a beneficial implication for memory management: while inner class instances always hold a strong reference to their enclosing instance, lambdas that do not capture members from the enclosing instance do not hold a reference to it. This characteristic of inner class instances can often be a source of memory leaks.

Note that this other behavior, inner class instances always holding an implicit reference to the outer this instance, also does not appear anywhere in the specification. So when even this behavior, causing more harm than good if ever being intentional, is taken for granted despite not appearing in the specification, we can be sure that the intentionally implemented behavior to overcome this issue will never be changed.

But if you’re still not convinced, you may follow the pattern shown in this answer or that answer of delegating to a static method to perform the Cleaner registration. This has the benefit of also preventing accidental use of members while still being simpler than the documentation’s suggested use of a nested static class.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • That does seem like as an authoritative answer as one could hope for outside the specification. And yeah, I've long wondered why inner classes *always* reference the outer instance even when the code doesn't actually reference it. It's kinda weird. – Dolda2000 Dec 06 '21 at 19:08
  • 2
    The behavior has been changed, inner classes [do not always capture `this` anymore](https://bugs.openjdk.org/browse/JDK-8271717) – Holger May 08 '23 at 10:26
1

I think you're safe. It's not an aspect of the JIT or a garbage collector implementation (stuff from "java.exe") ; this is done directly by the compiler ("javac.exe"). It's not going to 'backslide' and inject useless and potentially pricey variables. It also means you are not dependent on a JVM's behaviour: you're merely dependent on a compiler's behaviour. For starters, there aren't all that many (ecj and javac that's pretty much it - all others you might be thinking of are forks of those, or are wrappers around those), and I'm pretty sure both ecj and javac don't capture the this now and presumably never will in the future.

A bigger issue is that javac certainly won't complain if you 'accidentally' do happen to capture anything that requires the this ref; that will lead to the this ref getting silently captured and ruining your cleanup library rather thoroughly. It feels like you've designed a library here where it's rather all too easy to shoot yourself in the foot.

I'm not quite sure what you can do to fix this. Possibly you can lean into it and use ASM or bytebuddy or similar to tear the class open1 and doublecheck that the this ref is not seeing capture. It's probably not worth the potentially sizable time it'd take to chase down all the refs to ensure that this isn't captured in a roundabout fashion (where the lambda captures variable y, and y has a field of type Bar pointing at some instance and that instance has a field whose value is a ref back to the original this, thus, preventing collection), but checking for direct capture is potentially interesting. Possibly even only in an assert statement so any testcase that does it will result in an AssertionError thrown, failing the test, letting you know this error was made.

[1] You can get the bytes of any class with String.class.getResourceAsStream("String.class") - you can read that InputStream and feed it into ASM / bytebuddy / etc. The costs of running a class through such a loop are considerable, of course.

rzwitserloot
  • 85,357
  • 5
  • 51
  • 72
  • I'm certainly aware of the difficulties of guaranteeing the non-capture of `this`, but I think they are inherent to using a `Cleaner`. Even if the method suggested in the `Cleaner` documentation of writing a static class that encapsulates the cleanup action doesn't really do anything for the cases you mention. In fairness, in reality I don't really do anything fancy with any cleanup action, I mostly just use them to emit warnings that I forgot to discard something where it should have been discarded. – Dolda2000 Dec 06 '21 at 02:08
  • While you're probably right that the reachability of `this` is determined by `javac`, I think that only moves the question to whether future versions of `javac` might insert references to `this`. I too would presume that they would have no reason to change it, but I'd like to not be surprised by faulty presumptions without warning. – Dolda2000 Dec 06 '21 at 02:15
  • @Dolda2000 Java does not guarantee backwards compatibility. That means you can't just blindly upgrade and it also means when relying on just about anything, specced or not, you need to take a moment to think about how likely it is some future version is going to break it. But, most things tend to 'last' and won't break on you. A modicum of common sense usually makes things clear. This one, in my opinion, __easily__ clears the bar. I'd use it without losing a wink of sleep. – rzwitserloot Dec 06 '21 at 02:23