Beware garbage collection when working with inner classes
If youโve read my Java 101 tutorial introducing static classes and inner classes, you should be familiar with the basics of working with nested classes in Java code. In this associated tip, Iโll walk you through one of the pitfalls of nesting classes, which is the inner classโs potential for causing a memory leak and out-of-memory error in the JVM.
This type of memory leak occurs because an inner class must at all times be able to access its outer classโwhich doesnโt always work with the JVMโs plans.
Getting from a simple nesting prodedure to an out-of-memory error (and possibly shutting down the JVM) is a process. The best way to understand it is by watching it unfold.
Step 1: An inner class references its outer class
Any instance of an inner class contains an implicit reference to its outer class. For example, consider the following declaration of EnclosingClass with its nested EnclosedClass non-static member class:
public class EnclosingClass
{
public class EnclosedClass
{
}
}
To better understand this connection, we can compile the above source code (javac EnclosingClass.java) into EnclosingClass.class and EnclosingClass$EnclosedClass.class, then examine the latter class file.
The JDK contains a javap (Java Print) tool for disassembling class files. On the command line, follow javap with EnclosingClass$EnclosedClass, as follows:
javap EnclosingClass$EnclosedClass
You should observe the following output, which reveals a synthetic (manufactured) final EnclosingClass
this$0 field that holds a reference to EnclosingClass:
Compiled from "EnclosingClass.java"
public class EnclosingClass$EnclosedClass {
final EnclosingClass this$0;
public EnclosingClass$EnclosedClass(EnclosingClass);
}
Step 2: The constructor captures the enclosing class reference
The above output reveals a constructor with an EnclosingClass parameter. Execute javap with the -v (verbose) option and youโll observe the constructor saving an EnclosingClass object reference in the this$0 field:
final EnclosingClass this$0;
descriptor: LEnclosingClass;
flags: (0x1010) ACC_FINAL, ACC_SYNTHETIC
public EnclosingClass$EnclosedClass(EnclosingClass);
descriptor: (LEnclosingClass;)V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:LEnclosingClass;
5: aload_0
6: invokespecial #2 // Method java/lang/Object."<init>":()V
9: return
LineNumberTable:
line 3: 0
Step 3: Declare a new method
Next, suppose you declare a method in another class that instantiates EnclosingClass, followed by EnclosedClass. The next code fragment reveals this instantiation sequence:
EnclosingClass ec = new EnclosingClass();
ec.new EnclosedClass();
The javap output below shows the bytecode translation for this source code. Line 18 reveals the call to EnclosingClass$EnclosedClass(EnclosingClass). This call is to save the enclosing class reference in the enclosed class:
0: new #2 // class EnclosingClass
3: dup
4: invokespecial #3 // Method EnclosingClass."<init>":()V
7: astore_1
8: new #4 // class EnclosingClass$EnclosedClass
11: dup
12: aload_1
13: dup
14: invokestatic #5 // Method java/util/Objects.requireNonNull:(Ljava/lang/Object;)Ljava/lang/Object;
17: pop
18: invokespecial #6 // Method EnclosingClass$EnclosedClass."<init>":(LEnclosingClass;)V
21: pop
22: return
Anatomy of a memory leak
In the above examples, weโve stored a reference of an enclosing class in a manufactured variable of the enclosed class. This can lead to a memory leak in which the enclosing class references a large graph of objects that cannot be garbage collected. Depending on the application code, itโs possible to exhaust memory and receive an out-of-memory error, resulting in termination of the JVM. The listing below demonstrates this scenario.
Listing 1. MemoryLeak.java
import java.util.ArrayList;
class EnclosingClass
{
private int[] data;
public EnclosingClass(int size)
{
data = new int[size];
}
class EnclosedClass
{
}
EnclosedClass getEnclosedClassObject()
{
return new EnclosedClass();
}
}
public class MemoryLeak
{
public static void main(String[] args)
{
ArrayList al = new ArrayList<>();
int counter = 0;
while (true)
{
al.add(new EnclosingClass(100000).getEnclosedClassObject());
System.out.println(counter++);
}
}
}
The EnclosingClass declares a private data field that references an array of integers. The arrayโs size is passed to this classโs constructor and the array is instantiated.
The EnclosingClass also declares EnclosedClass, a nested non-static member class, and a method that instantiates EnclosedClass, returning this instance.
MemoryLeakโs main() method first creates a java.util.ArrayList to store EnclosingClass.EnclosedClass objects. Ignore the use of packages and generics for now, along with ArrayList (which stores objects in a dynamic array)โthe important point is to observe how the memory leak occurs.
After initializing a counter to 0, main() enters an infinite while loop that repeatedly instantiates EnclosedClass and adds it to the array list. It then prints (or increments) the counter. Before the enclosed class can be instantiated, EnclosingClass must be instantiated, with 100000 being passed as the array size.
Each stored EnclosedClass object maintains a reference to its enclosing object, which references an array of 100,000 32-bit integers (or 400,000 bytes). This outer object cannot be garbage collected until the inner object is garbage collected. Eventually, this application will exhaust memory.
Compile Listing 1 as follows:
javac MemoryLeak.java
Run the application as follows:
java MemoryLeak
I observe the following suffix of the outputโnote that you might observe a different final counter value:
7639
7640
7641
7642
7643
7644
7645
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at EnclosingClass.<init>(MemoryLeak.java:9)
at MemoryLeak.main(MemoryLeak.java:30)
OutOfMemoryError is an example of a Java exception. See Exceptions in Java, Part 1 for more about throwing and handling Java exceptions in your programs.


