Avoid memory leaks in inner classes

tip
Feb 20, 20205 mins
JavaProgramming Languages

Beware garbage collection when working with inner classes

Deep, nested topographical layers / binary code [GREEN]
Credit: FILO / Aleksei Derin / Getty Images

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.