内部类有在JVM中导致内存泄漏和内存不足错误的可能性。
之所以会发生这种类型的内存泄漏,是因为内部类必须始终能够访问其外部类-其实这并非JVM期望的。
从简单的嵌套过程到内存不足错误(并可能关闭JVM)是一个过程。理解它的最好方法是观察它的发展。
步骤1:内部类引用其外部类
内部类的任何实例都包含对其外部类的隐式引用。例如,考虑以下EnclosingClass带有嵌套的EnclosedClass非静态成员类的声明:
public class EnclosingClass{ public class EnclosedClass { }}
为了更好地理解这种联系,我们可以将上面的源代码(javac EnclosingClass.java)编译为EnclosingClass.class和EnclosingClass$EnclosedClass.class,然后检查后者的类文件
javap EnclosingClass$EnclosedClass
应该观察以下输出,该输出显示了包含以下内容的 final EnclosingClass this$0包含字段EnclosingClass的引用。
Compiled from "EnclosingClass.java"public class EnclosingClass$EnclosedClass { final EnclosingClass this$0; public EnclosingClass$EnclosedClass(EnclosingClass);}
步骤2:构造函数获取封闭的类引用
上面的输出显示了带有EnclosingClass参数的构造函数。javap使用-v(详细)选项执行,您将观察到构造函数将EnclosingClass对象引用保存在this$0字段中:
final EnclosingClass this$0; descriptor: LEnclosingClass; flags: (0x1010) ACC_FINAL, ACC_SYNTHETICpublic 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."":()V 9: return LineNumberTable: line 3: 0
步骤3:声明一种新方法
接下来,假设在另一个实例化的类中声明了一个方法EnclosingClass,然后是EnclosedClass。下一个代码片段揭示了此实例化序列:
EnclosingClass ec = new EnclosingClass();ec.new EnclosedClass();
以下javap输出显示了此源代码的字节码转换。第18行显示了对的调用EnclosingClass$EnclosedClass(EnclosingClass)。该调用是为了将enclosing类引用保存在enclosing类中:
0: new #2 // class EnclosingClass 3: dup 4: invokespecial #3 // Method EnclosingClass."":()V 7: astore_1 8: new #4 // class EnclosingClass$EnclosedClass11: dup12: aload_113: dup14: invokestatic #5 // Method java/util/Objects.requireNonNull:(Ljava/lang/Object;)Ljava/lang/Object;17: pop18: invokespecial #6 // Method EnclosingClass$EnclosedClass."":(LEnclosingClass;)V21: pop22: return
内存泄漏的解剖
在以上示例中,我们已将封闭类的引用存储在封闭类的制造变量中。这可能导致内存泄漏,其中的封闭类引用了无法垃圾回收的大型对象图。根据应用程序代码,可能会耗尽内存并收到内存不足错误,从而导致JVM终止。下面的代码演示了这种情况。
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++); } }}
该EnclosingClass声明一个私有data引用整数数组领域。数组的大小传递给此类的构造函数,并实例化该数组。
EnclosingClass还声明EnclosedClass,一个嵌套非静态成员的类,和一种方法,其实例化EnclosedClass,返回此实例
MemoryLeak的main()方法首先创建一个java.util.ArrayList存储EnclosingClass.EnclosedClass对象,开始观察内存泄漏
将计数器初始化为0后,main()进入无限while循环,该循环重复实例化EnclosedClass并将其添加到数组列表中。然后打印(或递增)计数器。在实例化封闭的类之前,EnclosingClass必须实例化该实例,并将100000其作为数组大小传递
每个存储的EnclosedClass对象维护对其封闭对象的引用,该对象引用由100,000个32位整数(或400,000字节)组成的数组。在对内部对象进行垃圾收集之前,无法对外部对象进行垃圾收集。最终,该应用程序将耗尽内存
7639764076417642764376447645Exception in thread "main" java.lang.OutOfMemoryError: Java heap spaceat EnclosingClass.(MemoryLeak.java:9)at MemoryLeak.main(MemoryLeak.java:30)