我先假設讀者已經熟悉在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>
最後,如果覺得本文不錯,別忘了點贊轉發一下! 更多的乾貨資料可以在下方評論區留言獲取,期待你的評論!
閱讀更多 享學課堂online 的文章