我先假设读者已经熟悉在Java代码中使用嵌套类的基础知识。在本文里,我将展示嵌套类的陷阱,内部类在JVM中引起内存泄漏和内存不足错误的地方。之所以会发生这种类型的内存泄漏,是因为内部类必须始终能够访问其外部类。从简单的嵌套过程到内存不足错误(并可能关闭JVM)是一个过程。我们一步步看他是如何产生的。
内部类的任何实例都包含对其外部类的隐式引用。例如,考虑以下带有嵌套的EnclosedClass非静态成员类的EnclosingClass声明:
1. `public class EnclosingClass`
2. `{`
3. `public class EnclosedClass`
4. `{`
5. `}`
6. `}`
为了更好地理解这种连接,我们可以将上面的源代码(javac EnclosingClass.java)编译为EnclosingClass.class和EnclosingClass $ EnclosedClass.class,然后检查后者的类文件。
JDK包含用于反汇编类文件的javap(Java打印)工具。在命令行上,使javap带有EnclosingClass $ EnclosedClass,如下所示:
1. `javap EnclosingClass$EnclosedClass`
我们可以观察到以下输出,该输出揭示了一个隐含的 final的 EnclosingClass this $ 0字段,该字段包含对EnclosingClass的引用:
1. `Compiled from "EnclosingClass.java"`
2. `public class EnclosingClass$EnclosedClass {`
3. `final EnclosingClass this$0;`
4. `public EnclosingClass$EnclosedClass(EnclosingClass);`
5. `}`
上面的输出显示了带有EnclosingClass参数的构造函数。使用-v(详细)选项执行javap,可以观察到构造函数在this $ 0字段中保存了EnclosingClass对象引用:
1. `final EnclosingClass this$0;`
2. `descriptor: LEnclosingClass;`
3. `flags: (0x1010) ACC_FINAL, ACC_SYNTHETIC`
5. `public EnclosingClass$EnclosedClass(EnclosingClass);`
6. `descriptor: (LEnclosingClass;)V`
7. `flags: (0x0001) ACC_PUBLIC`
8. `Code:`
9. `stack=2, locals=2, args_size=2`
10. `0: aload_0`
11. `1: aload_1`
12. `2: putfield #1 // Field this$0:LEnclosingClass;`
13. `5: aload_0`
14. `6: invokespecial #2 // Method java/lang/Object."":()V`
15. `9: return`
16. `LineNumberTable:`
17. `line 3: 0`
接下来,我们另一个类中声明一个方法,实例化EnclosingClass,然后实例化EnclosedClass。例如:
1. `EnclosingClass ec = newEnclosingClass();`
2. `ec.newEnclosedClass();`
下面的javap输出显示了此源代码的字节码转换。第18行显示对EnclosingClass $ EnclosedClass(EnclosingClass)的调用。
1. `0: new #2 // class EnclosingClass`
2. `3: dup`
3. `4: invokespecial #3 // Method EnclosingClass."":()V`
4. `7: astore_1`
5. `8: new #4 // class EnclosingClass$EnclosedClass`
6. `11: dup`
7. `12: aload_1`
8. `13: dup`
9. `14: invokestatic #5 // Method java/util/Objects.requireNonNull:(Ljava/lang/Object;)Ljava/lang/Object;`
10. `17: pop`
11. `18: invokespecial #6 // Method EnclosingClass$EnclosedClass."":(LEnclosingClass;)V`
12. `21: pop`
13. `22: return`
在以上示例中,根据应用程序代码,可能会耗尽内存并收到内存不足错误,从而导致JVM终止。下面的清单演示了这种情况。
1. `import java.util.ArrayList;`
3. `class EnclosingClass`
4. `{`
5. `private int[] data;`
7. `public EnclosingClass(int size)`
8. `{`
9. `data = new int[size];`
10. `}`
12. `class EnclosedClass`
13. `{`
14. `}`
16. `EnclosedClass getEnclosedClassObject()`
17. `{`
18. `return new EnclosedClass();`
19. `}`
20. `}`
22. `public class MemoryLeak`
23. `{`
24. `public static void main(String[] args)`
25. `{`
26. `ArrayList al = new ArrayList<>();`
27. `int counter = 0;`
28. `while (true)`
29. `{`
30. `al.add(new EnclosingClass(100000).getEnclosedClassObject());`
31. `System.out.println(counter++);`
32. `}`
33. `}`
34. `}`
EnclosingClass声明一个引用整数数组的私有数据字段。数组的大小传递给此类的构造函数,并实例化该数组。
EnclosingClass还声明EnclosedClass,一个嵌套的非静态成员类,以及一个实例化EnclosedClass的方法,并返回此实例。
MemoryLeak的main()方法首先创建一个java.util.ArrayList来存储EnclosingClass.EnclosedClass对象。现在,观察内存泄漏是如何发生的。
将计数器初始化为0后,main()进入无限while循环,该循环重复实例化EnclosedClass并将其添加到数组列表中。然后打印(或递增)计数器。
每个存储的EnclosedClass对象都维护对其外部对象的引用,该对象引用100,000个32位整数(或400,000字节)的数组。在对内部对象进行垃圾收集之前,无法对外部对象进行垃圾收集。最终,该应用程序将耗尽内存。
1. `javac MemoryLeak.java`
2. `java MemoryLeak`
我们将观察到如下输出(当然在不同的机器上,最后的数字可能不一样):
1. `7639`
2. `7640`
3. `7641`
4. `7642`
5. `7643`
6. `7644`
7. `7645`
8. `Exception in thread "main" java.lang.OutOfMemoryError: Java heap space`
9. `at EnclosingClass.(MemoryLeak.java:9)`
10. `at MemoryLeak.main(MemoryLeak.java:30)`
最后,如果觉得本文不错,别忘了点赞转发一下! 更多的干货资料可以在下方评论区留言获取,期待你的评论!