干货详解:一文教你如何避免内部类中的内存泄漏

我先假设读者已经熟悉在Java代码中使用嵌套类的基础知识。在本文里,我将展示嵌套类的陷阱,内部类在JVM中引起内存泄漏和内存不足错误的地方。之所以会发生这种类型的内存泄漏,是因为内部类必须始终能够访问其外部类。从简单的嵌套过程到内存不足错误(并可能关闭JVM)是一个过程。我们一步步看他是如何产生的。

步骤1:内部类引用其外部类

内部类的任何实例都包含对其外部类的隐式引用。例如,考虑以下带有嵌套的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.  `}`

步骤2:构造函数获取封闭的类引用

上面的输出显示了带有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`

步骤3:声明一个新方法

接下来,我们另一个类中声明一个方法,实例化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)`

最后,如果觉得本文不错,别忘了点赞转发一下! 更多的干货资料可以在下方评论区留言获取,期待你的评论!

你可能感兴趣的:(编程人生,编程语言,程序员)