静态内部类如何避免内存泄露
如果您已阅读介绍静态类和内部类的 Java 101 教程 ,则应该熟悉在Java代码中使用嵌套类的基础知识。 在这个相关的技巧中,我将带您了解嵌套类的陷阱之一,这是内部类在JVM中引起内存泄漏和内存不足错误的潜力。
之所以会发生这种类型的内存泄漏,是因为内部类必须始终能够访问其外部类-并非总是与JVM的计划一起使用。
从简单的嵌套过程到内存不足错误(并可能关闭JVM)是一个过程。 理解它的最好方法是观看它的展开。
内部类的任何实例都包含对其外部类的隐式引用。 例如,考虑以下带有嵌套的EnclosedClass
非静态成员类的EnclosingClass
声明:
public class EnclosingClass
{
public class EnclosedClass
{
}
}
为了更好地理解这种联系,我们可以将上述源代码( javac EnclosingClass.java
) javac EnclosingClass.java
为EnclosingClass.class
和EnclosingClass$EnclosedClass.class
,然后检查后者的类文件。
JDK包含用于反汇编类文件的javap
(Java打印)工具。 在命令行上,使javap
带有EnclosingClass$EnclosedClass
,如下所示:
javap EnclosingClass$EnclosedClass
您应该观察以下输出,该输出揭示了一个合成的 (制造的) final EnclosingClass this$0
字段, final EnclosingClass this$0
字段包含对EnclosingClass
的引用:
Compiled from "EnclosingClass.java"
public class EnclosingClass$EnclosedClass {
final EnclosingClass this$0;
public EnclosingClass$EnclosedClass(EnclosingClass);
}
上面的输出显示了带有EnclosingClass
参数的构造函数。 使用-v
(详细)选项执行javap
,您将观察到构造函数在this$0
字段中保存了EnclosingClass
对象引用:
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."":()V
9: return
LineNumberTable:
line 3: 0
接下来,假设您在另一个实例化EnclosingClass
类中声明一个方法,然后实例化EnclosedClass
。 下一个代码片段揭示了此实例化序列:
EnclosingClass ec = new EnclosingClass();
ec.new EnclosedClass();
下面的javap
输出显示了此源代码的字节码转换。 第18行显示对EnclosingClass$EnclosedClass(EnclosingClass)
的调用。 该调用是为了将封闭的类引用保存在封闭的类中:
0: new #2 // class EnclosingClass
3: dup
4: invokespecial #3 // Method EnclosingClass."":()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."":(LEnclosingClass;)V
21: pop
22: 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
对象。 现在,请忽略包和泛型以及ArrayList
(将对象存储在动态数组中)的使用-重要的一点是观察内存泄漏是如何发生的。
将计数器初始化为0后, main()
进入无限while
循环,该循环重复实例化EnclosedClass
并将其添加到数组列表中。 然后打印(或递增)计数器。 封闭类可以被实例化之前, EnclosingClass
必须被实例化,以100000
被作为阵列尺寸通过。
每个存储的EnclosedClass
对象都维护对其封闭对象的引用,该对象引用100,000个32位整数(或400,000字节)组成的数组。 在对内部对象进行垃圾收集之前,无法对外部对象进行垃圾收集。 最终,该应用程序将耗尽内存。
如下编译清单1:
javac MemoryLeak.java
运行应用程序,如下所示:
java MemoryLeak
我观察到输出的以下后缀-请注意,您可能会观察到不同的最终计数器值:
7639
7640
7641
7642
7643
7644
7645
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at EnclosingClass.(MemoryLeak.java:9)
at MemoryLeak.main(MemoryLeak.java:30)
OutOfMemoryError
是Java异常的示例。 有关在程序中引发和处理Java异常的更多信息,请参见Java异常,第1部分 。
这个故事“避免内部类中的内存泄漏”最初是由JavaWorld发布的 。
翻译自: https://www.infoworld.com/article/3526554/avoid-memory-leaks-in-inner-classes.html
静态内部类如何避免内存泄露