如何避免內部類中的內存洩漏

我先假設讀者已經熟悉在Java代碼中使用嵌套類的基礎知識。在本文裡,我將展示嵌套類的陷阱,內部類在JVM中引起內存洩漏和內存不足錯誤的地方。之所以會發生這種類型的內存洩漏,是因為內部類必須始終能夠訪問其外部類。從簡單的嵌套過程到內存不足錯誤(並可能關閉JVM)是一個過程。我們一步步看他是如何產生的。

步驟1:內部類引用其外部類

內部類的任何實例都包含對其外部類的隱式引用。例如,考慮以下帶有嵌套的EnclosedClass非靜態成員類的EnclosingClass聲明:

<code>
1. `public class EnclosingClass`

2. `{`

3. `public class EnclosedClass`

4. `{`

5. `}`

6. `}`

/<code>

為了更好地理解這種連接,我們可以將上面的源代碼(javac EnclosingClass.java)編譯為EnclosingClass.class和EnclosingClass $ EnclosedClass.class,然後檢查後者的類文件。

JDK包含用於反彙編類文件的javap(Java打印)工具。在命令行上,使javap帶有EnclosingClass $ EnclosedClass,如下所示:

<code>
1. `javap EnclosingClass$EnclosedClass`

/<code>

我們可以觀察到以下輸出,該輸出揭示了一個隱含的 final的 EnclosingClass this $ 0字段,該字段包含對EnclosingClass的引用:

<code> 

1. `Compiled from "EnclosingClass.java"`

2. `public class EnclosingClass$EnclosedClass {`

3. `final EnclosingClass this$0;`

4. `public EnclosingClass$EnclosedClass(EnclosingClass);`

5. `}`

/<code>

步驟2:構造函數獲取封閉的類引用

上面的輸出顯示了帶有EnclosingClass參數的構造函數。使用-v(詳細)選項執行javap,可以觀察到構造函數在this $ 0字段中保存了EnclosingClass對象引用:

<code>
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."<init>":()V`


15. `9: return`

16. `LineNumberTable:`

17. `line 3: 0`

/<init>/<code>

接下來,我們另一個類中聲明一個方法,實例化EnclosingClass,然後實例化EnclosedClass。例如:

<code>
1. `EnclosingClass ec = newEnclosingClass();`

2. `ec.newEnclosedClass();`

/<code>

下面的javap輸出顯示了此源代碼的字節碼轉換。第18行顯示對EnclosingClass $ EnclosedClass(EnclosingClass)的調用。

<code>
1. `0: new #2 // class EnclosingClass`

2. `3: dup`

3. `4: invokespecial #3 // Method EnclosingClass."<init>":()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."<init>":(LEnclosingClass;)V`

12. `21: pop`


13. `22: return`

/<init>/<init>/<code>

內存洩漏的解剖

在以上示例中,根據應用程序代碼,可能會耗盡內存並收到內存不足錯誤,從而導致JVM終止。下面的清單演示了這種情況。

<code>
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. `}`

/<code>

EnclosingClass聲明一個引用整數數組的私有數據字段。數組的大小傳遞給此類的構造函數,並實例化該數組。

EnclosingClass還聲明EnclosedClass,一個嵌套的非靜態成員類,以及一個實例化EnclosedClass的方法,並返回此實例。

MemoryLeak的main()方法首先創建一個java.util.ArrayList來存儲EnclosingClass.EnclosedClass對象。現在,觀察內存洩漏是如何發生的。

將計數器初始化為0後,main()進入無限while循環,該循環重複實例化EnclosedClass並將其添加到數組列表中。然後打印(或遞增)計數器。

每個存儲的EnclosedClass對象都維護對其外部對象的引用,該對象引用100,000個32位整數(或400,000字節)的數組。在對內部對象進行垃圾收集之前,無法對外部對象進行垃圾收集。最終,該應用程序將耗盡內存。

<code>
1. `javac MemoryLeak.java`

2. `java MemoryLeak`
/<code>

我們將觀察到如下輸出(當然在不同的機器上,最後的數字可能不一樣):

<code>
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.<init>(MemoryLeak.java:9)`

10. `at MemoryLeak.main(MemoryLeak.java:30)`

/<init>/<code>

最後,如果覺得本文不錯,別忘了點贊轉發一下! 更多的乾貨資料可以在下方評論區留言獲取,期待你的評論!


分享到:


相關文章: