nested and inner classes
static nested, inner, local, anonymous. and the hidden this$0.
- chapter
- chapter 03.11
- reading time
- 6 min read
a class declared inside another comes in four kinds: static nested (just a class with a longer name), inner (every instance holds a hidden reference, this$0, to the outer object that made it), local and anonymous (declared inside a method, and they also copy the local variables they use into hidden fields). that hidden outer reference is the classic java memory leak.
the picture
think of a backpack (the outer object) and a keychain clipped onto it (an inner object). you can carry the keychain around on its own, but it's still clipped to the backpack, so wherever the keychain goes, the backpack comes too. a static nested class is a keychain sold separately: same brand, no clip.
class Outer {
String name;
class Inner {
String hi() { return name; }
}
}
var i = outer.new Inner();an inner class reads Outer's fields, so every Inner carries a hidden reference to the Outer that made it. you can't even create one without an Outer: outer.new Inner().
javap -p). the memory pane shows one instance: watch for the this$0 arrow back to the Outer, and its 10 MB big array riding along.| kind | declared | needs an outer object? | can see |
|---|---|---|---|
| static nested | static class X in a class | no | outer's static members, and its privates |
| inner | class X in a class | yes: outer.new X() | everything in the outer object |
| local | class X inside a method | if the method isn't static | the outer object, and effectively final locals |
| anonymous | new Iface() {"{ ... }"} | if the method isn't static | same as local |
proof: the hidden fields are real
import java.lang.reflect.Field;
import java.util.function.Supplier;
public class Main {
private String name = "outer";
class Inner {
String hi() { return "inner of " + name; }
}
static class Nested {
String hi() { return "nested"; }
}
Supplier<String> anon(String greeting) {
return new Supplier<>() {
public String get() { return greeting + ", " + name; }
};
}
static void fields(Object o) {
StringBuilder sb = new StringBuilder(o.getClass().getName() + ":");
for (Field f : o.getClass().getDeclaredFields()) sb.append(" ").append(f.getName());
System.out.println(sb);
}
public static void main(String[] args) {
Main outer = new Main();
Main.Inner inner = outer.new Inner();
Main.Nested nested = new Main.Nested();
Supplier<String> s = outer.anon("hi");
fields(inner);
fields(nested);
fields(s);
System.out.println(inner.hi() + " / " + s.get());
}
}Main$Inner: this$0 Main$Nested: Main$1: val$greeting this$0 inner of outer / hi, outer
this one needs a real JVM (reflection), so it doesn't run in the browser. copy it into java Main.java to try it.
you never wrote a field called this$0 or val$greeting. javac did. the inner class reads name, so it needs the Main. the anonymous class reads greeting, which lives in a stack frame that will be gone by the time get() runs, so javac copies it into the object.
turns out javac skips this$0 when it can (and constants fool you)+
since jdk 18 (JDK-8271623), javac only emits this$0 if the inner class actually uses the outer instance. an inner class that never touches Outer still takes an Outer constructor parameter, but throws it away, so it can't leak it. (serializable inner classes without a serialVersionUID still get the field, for compatibility.)
and a sneaky one: if name above were private final String name = "outer";, it'd be a compile-time constant, javac would fold "inner of " + name into "inner of outer", and Inner would have no this$0 at all. same source, different class file. fun fact for compact source files: their top-level classes become inner classes of the implicit class, so class Dog in a void main() file compiles to Main$Dog, whose constructor takes a hidden Main parameter.
the leak
an inner, local or anonymous object that outlives its outer object keeps the outer object alive. register one as a listener in something long-lived, and the whole outer object (and everything it points to) can never be collected.
- 1import java.util.*;
- 2
- 3public class Main {
- 4 static final List<Runnable> listeners = new ArrayList<>();
- 5
- 6 static class Screen {
- 7 final byte[] pixels = new byte[10_000_000];
- 8
- 9 void open() {
- 10 listeners.add(new Runnable() {
- 11 public void run() { redraw(); }
- 12 });
- 13 }
- 14
- 15 void redraw() { }
- 16 }
- 17
- 18 public static void main(String[] args) {
- 19 Screen s = new Screen();
- 20 s.open();
- 21 s = null;
- 22 System.out.println(listeners.size());
- 23 }
- 24}
·a long-lived static list of listeners. it lives as long as the class does, which is basically forever.
s = null, and the Screen is still reachable through the static list, the listener, and its hidden this$0. no ghost appears because nothing became unreachable. that's the bug.the fixes, in order of preference:
- make it
static(a static nested class, or a top-level one) and pass in only what it needs. nothis$0possible. - use a lambda that doesn't touch
this. a lambda only captures what it uses; if it never mentions an instance member, it doesn't capture the outer object. see lambdas. - unregister listeners when the owner is done (
close(),dispose()), or hold them through aWeakReference.
more ways to leak, and how to find them in a heap dump: memory leaks.
capturing locals: effectively final
local and anonymous classes (and lambdas) copy captured locals into fields when they're created. if java let you change the local afterwards, the copy and the original would silently disagree. so the rule is: you can only capture a local that's effectively final (never assigned after initialization).
void main() {
int count = 0;
Runnable r = new Runnable() {
public void run() {
IO.println(count);
}
};
count++;
}error: local variables referenced from an inner class must be final or effectively final. the lambda version says the same thing with "a lambda expression" in it. the usual escape hatch is a one-element array or an AtomicInteger, which keeps the reference final while the contents change. fields are different: an inner class reads them live through this$0, so they can change freely.
- static nested: a normal class with a longer name. no outer object.
- inner: every instance holds
this$0, a hidden reference to its outer object. - local and anonymous:
this$0plus copies of captured locals (val$x). - captured locals must be effectively final, because the class holds a copy.
- an inner object that outlives its outer keeps it alive. default to static.