使用内部类时要当心垃圾收集
如果您已了解静态类和内部类,则应该熟悉使用Java代码中的嵌套类的基础知识。在这个相关的技巧中,我将带您了解嵌套类的陷阱之一,这是内部类在JVM中导致内存泄漏和内存不足错误的潜力。
之所以会发生这种类型的内存泄漏,是因为内部类必须始终能够访问其外部类-并非总是与JVM的计划一起使用。
从简单的嵌套过程到内存不足错误(并可能关闭JVM)是一个过程。理解它的最好方法是看它的源码。
内部类的任何实例都包含对其外部类的隐式引用。例如,考虑以下EnclosingClass带有嵌套的EnclosedClass非静态成员类的声明:
public class EnclosingClass
{
public class EnclosedClass
{
}
}
为了更好地理解这种联系,我们可以将上面的源代码(javac EnclosingClass.java)编译为EnclosingClass.class和EnclosingClass$EnclosedClass.class,然后检查后者的类文件。
JDK包含一个javap(Java打印)工具,用于反汇编类文件。在命令行上,使用javap运行EnclosingClass$EnclosedClass如下:
javap EnclosingClass$EnclosedClass
您可以看到以下输出,该输出显示了包含以下内容final EnclosingClass this$0字段EnclosingClass:
public class com.github.crab2died.EnclosingClass$EnclosedClass {
final com.github.crab2died.EnclosingClass this$0;
public com.github.crab2died.EnclosingClass$EnclosedClass(com.github.crab2died.EnclosingClass);
}
步骤2:构造函数获取封闭的类引用
上面的输出显示了带有EnclosingClass参数的构造函数。使用javap -v(verbose)选项执行,您将观察到构造函数将EnclosingClass对象引用保存在this$0字段中:
Classfile /D:/aplus/EnclosingClass$EnclosedClass.class
Last modified 2020-3-15; size 440 bytes
MD5 checksum 308ea24edb49a4d49669d101fff55d5a
Compiled from "EnclosingClass.java"
public class com.github.crab2died.EnclosingClass$EnclosedClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Fieldref #3.#13 // com/github/crab2died/EnclosingClass$EnclosedClass.this$0:Lcom/github/crab2died/EnclosingClass;
#2 = Methodref #4.#14 // java/lang/Object."":()V
#3 = Class #16 // com/github/crab2died/EnclosingClass$EnclosedClass
#4 = Class #19 // java/lang/Object
#5 = Utf8 this$0
#6 = Utf8 Lcom/github/crab2died/EnclosingClass;
#7 = Utf8
#8 = Utf8 (Lcom/github/crab2died/EnclosingClass;)V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 SourceFile
#12 = Utf8 EnclosingClass.java
#13 = NameAndType #5:#6 // this$0:Lcom/github/crab2died/EnclosingClass;
#14 = NameAndType #7:#20 // "":()V
#15 = Class #21 // com/github/crab2died/EnclosingClass
#16 = Utf8 com/github/crab2died/EnclosingClass$EnclosedClass
#17 = Utf8 EnclosedClass
#18 = Utf8 InnerClasses
#19 = Utf8 java/lang/Object
#20 = Utf8 ()V
#21 = Utf8 com/github/crab2died/EnclosingClass
{
final com.github.crab2died.EnclosingClass this$0;
descriptor: Lcom/github/crab2died/EnclosingClass;
flags: ACC_FINAL, ACC_SYNTHETIC
public com.github.crab2died.EnclosingClass$EnclosedClass(com.github.crab2died.EnclosingClass);
descriptor: (Lcom/github/crab2died/EnclosingClass;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:Lcom/github/crab2died/EnclosingClass;
5: aload_0
6: invokespecial #2 // Method java/lang/Object."":()V
9: return
LineNumberTable:
line 12: 0
}
SourceFile: "EnclosingClass.java"
InnerClasses:
public #17= #3 of #15; //EnclosedClass=class com/github/crab2died/EnclosingClass$EnclosedClass of class com/github/crab2died/EnclosingClass
步骤3:声明一种新方法
在实例化类中声明了EnclosingClass,然后创建EnclosedClass。
EnclosingClass ec = new EnclosingClass();
ec.new EnclosedClass();
Last modified 2020-3-15; size 502 bytes
MD5 checksum d31832f98dbbf557e995ac447cc55fb2
Compiled from "EnclosingClass.java"
public class com.github.crab2died.EnclosingClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#17 // java/lang/Object."":()V
#2 = Class #18 // com/github/crab2died/EnclosingClass
#3 = Methodref #2.#17 // com/github/crab2died/EnclosingClass."":()V
#4 = Class #19 // com/github/crab2died/EnclosingClass$EnclosedClass
#5 = Methodref #7.#20 // java/lang/Object.getClass:()Ljava/lang/Class;
#6 = Methodref #4.#21 // com/github/crab2died/EnclosingClass$EnclosedClass."":(Lcom/github/crab2died/EnclosingClass;)V
#7 = Class #22 // java/lang/Object
#8 = Utf8 EnclosedClass
#9 = Utf8 InnerClasses
#10 = Utf8
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 test
#15 = Utf8 SourceFile
#16 = Utf8 EnclosingClass.java
#17 = NameAndType #10:#11 // "":()V
#18 = Utf8 com/github/crab2died/EnclosingClass
#19 = Utf8 com/github/crab2died/EnclosingClass$EnclosedClass
#20 = NameAndType #23:#24 // getClass:()Ljava/lang/Class;
#21 = NameAndType #10:#25 // "":(Lcom/github/crab2died/EnclosingClass;)V
#22 = Utf8 java/lang/Object
#23 = Utf8 getClass
#24 = Utf8 ()Ljava/lang/Class;
#25 = Utf8 (Lcom/github/crab2died/EnclosingClass;)V
{
public com.github.crab2died.EnclosingClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 10: 0
public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=4, locals=2, args_size=1
0: new #2 // class com/github/crab2died/EnclosingClass
3: dup
4: invokespecial #3 // Method "":()V
7: astore_1
8: new #4 // class com/github/crab2died/EnclosingClass$EnclosedClass
11: dup
12: aload_1
13: dup
14: invokevirtual #5 // Method java/lang/Object.getClass:()Ljava/lang/Class;
17: pop
18: invokespecial #6 // Method com/github/crab2died/EnclosingClass$EnclosedClass."":(Lcom/github/crab2died/EnclosingClass;)V
21: pop
22: return
LineNumberTable:
line 17: 0
line 18: 8
line 19: 22
}
SourceFile: "EnclosingClass.java"
InnerClasses:
public #8= #4 of #2; //EnclosedClass=class com/github/crab2died/EnclosingClass$EnclosedClass of class com/github/crab2died/EnclosingClass
内存泄漏的解剖
在以上示例中,我们已将封闭类的引用存储在封闭类的制造变量中。这可能导致内存泄漏,其中的封闭类引用了无法垃圾回收的大型对象图。根据应用程序代码,可能会耗尽内存并收到内存不足错误,从而导致JVM终止。下面的例子演示这种情况。
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++);
}
}
}
该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字节)的数组。在对内部对象进行垃圾收集之前,无法对外部对象进行垃圾收集。最终,该应用程序将耗尽内存。
我观察到输出的以下后缀-请注意,您可能会观察到不同的最终计数器值:
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)