欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

如何避免内部类中的内存泄漏

程序员文章站 2022-04-19 18:43:24
...

如何避免内部类中的内存泄漏

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

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

内部类的任何实例都包含对其外部类的隐式引用。 例如,考虑以下带有嵌套的EnclosedClass非静态成员类的EnclosingClass声明:

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的引用:

Compiled from "EnclosingClass.java"
public class EnclosingClass$EnclosedClass {
  final EnclosingClass this$0;
  public EnclosingClass$EnclosedClass(EnclosingClass);
}

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

上面的输出显示了带有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."<init>":()V
       9: return
    LineNumberTable:
      line 3: 0

步骤3:声明一个新方法

接下来,我们另一个类中声明一个方法,实例化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."<init>":()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."<init>":(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声明一个引用整数数组的私有数据字段。数组的大小传递给此类的构造函数,并实例化该数组。

EnclosingClass还声明EnclosedClass,一个嵌套的非静态成员类,以及一个实例化EnclosedClass的方法,并返回此实例。

MemoryLeak的main()方法首先创建一个java.util.ArrayList来存储EnclosingClass.EnclosedClass对象。现在,观察内存泄漏是如何发生的。

将计数器初始化为0后,main()进入无限while循环,该循环重复实例化EnclosedClass并将其添加到数组列表中。然后打印(或递增)计数器。

每个存储的EnclosedClass对象都维护对其外部对象的引用,该对象引用100,000个32位整数(或400,000字节)的数组。在对内部对象进行垃圾收集之前,无法对外部对象进行垃圾收集。最终,该应用程序将耗尽内存。

javac MemoryLeak.java
java MemoryLeak

我们将观察到如下输出(当然在不同的机器上,最后的数字可能不一样):

7639
7640
7641
7642
7643
7644
7645
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at EnclosingClass.<init>(MemoryLeak.java:9)
	at MemoryLeak.main(MemoryLeak.java:30)