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.
This story, "Avoid memory leaks in inner classes" was originally published by JavaWorld.