問題描述
UPD 21.11.2017:該錯誤已在 JDK 中修復,請參閱 Vicente Romero 的評論
UPD 21.11.2017: the bug is fixed in JDK, see comment from Vicente Romero
總結:
如果 for
語句用于任何 Iterable
實現(xiàn),則集合將保留在堆內(nèi)存中,直到當前范圍(方法、語句體)結束,并且即使您沒有對集合的任何其他引用并且應用程序需要分配新內(nèi)存,也不會被垃圾回收.
If for
statement is used for any Iterable
implementation the collection will remain in the heap memory till the end of current scope (method, statement body) and won't be garbage collected even if you don't have any other references to the collection and the application needs to allocate a new memory.
http://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8175883
https://bugs.openjdk.java.net/browse/JDK-8175883
例子:
如果我有下一個代碼,它會分配一個包含隨機內(nèi)容的大字符串列表:
If i have the next code, which allocates a list of large strings with random content:
import java.util.ArrayList;
public class IteratorAndGc {
// number of strings and the size of every string
static final int N = 7500;
public static void main(String[] args) {
System.gc();
gcInMethod();
System.gc();
showMemoryUsage("GC after the method body");
ArrayList<String> strings2 = generateLargeStringsArray(N);
showMemoryUsage("Third allocation outside the method is always successful");
}
// main testable method
public static void gcInMethod() {
showMemoryUsage("Before first memory allocating");
ArrayList<String> strings = generateLargeStringsArray(N);
showMemoryUsage("After first memory allocation");
// this is only one difference - after the iterator created, memory won't be collected till end of this function
for (String string : strings);
showMemoryUsage("After iteration");
strings = null; // discard the reference to the array
// one says this doesn't guarantee garbage collection,
// Oracle says "the Java Virtual Machine has made a best effort to reclaim space from all discarded objects".
// but no matter - the program behavior remains the same with or without this line. You may skip it and test.
System.gc();
showMemoryUsage("After force GC in the method body");
try {
System.out.println("Try to allocate memory in the method body again:");
ArrayList<String> strings2 = generateLargeStringsArray(N);
showMemoryUsage("After secondary memory allocation");
} catch (OutOfMemoryError e) {
showMemoryUsage("!!!! Out of memory error !!!!");
System.out.println();
}
}
// function to allocate and return a reference to a lot of memory
private static ArrayList<String> generateLargeStringsArray(int N) {
ArrayList<String> strings = new ArrayList<>(N);
for (int i = 0; i < N; i++) {
StringBuilder sb = new StringBuilder(N);
for (int j = 0; j < N; j++) {
sb.append((char)Math.round(Math.random() * 0xFFFF));
}
strings.add(sb.toString());
}
return strings;
}
// helper method to display current memory status
public static void showMemoryUsage(String action) {
long free = Runtime.getRuntime().freeMemory();
long total = Runtime.getRuntime().totalMemory();
long max = Runtime.getRuntime().maxMemory();
long used = total - free;
System.out.printf(" %40s: %10dk of max %10dk%n", action, used / 1024, max / 1024);
}
}
用有限的內(nèi)存編譯和運行它,像這樣(180mb):
compile and run it with limited memory, like this (180mb):
javac IteratorAndGc.java && java -Xms180m -Xmx180m IteratorAndGc
在運行時我有:
在第一次分配內(nèi)存之前:1251k of max 176640k
Before first memory allocating: 1251k of max 176640k
第一次內(nèi)存分配后:131426k of max 176640k
After first memory allocation: 131426k of max 176640k
迭代后:131426k of max 176640k
After iteration: 131426k of max 176640k
在方法體中強制 GC 后:最大 176640k 的 110682k(幾乎沒有收集到)
After force GC in the method body: 110682k of max 176640k (almost nothing collected)
再次嘗試在方法體中分配內(nèi)存:
Try to allocate memory in the method body again:
!!!! Out of memory error !!!!: 168948k of max 176640k
方法體后的GC:459k of max 176640k(垃圾被收集了!)
GC after the method body: 459k of max 176640k (the garbage is collected!)
方法外第三次分配總是成功:117740k of max 163840k
Third allocation outside the method is always successful: 117740k of max 163840k
所以,在 gcInMethod() 內(nèi)部,我嘗試分配列表,對其進行迭代,丟棄對列表的引用,(可選)強制垃圾收集并再次分配類似的列表.但由于內(nèi)存不足,我無法分配第二個數(shù)組.
So, inside gcInMethod() i tried to allocate the list, iterate over it, discard the reference to the list, (optional)force garbage collection and allocate similar list again. But i can't allocate second array because of lack of memory.
同時,在函數(shù)體之外,我可以成功強制垃圾回收(可選)并再次分配相同的數(shù)組大小!
In the same time, outside the function body i can successfully force garbage collection (optional) and allocate the same array size again!
為了避免函數(shù)體內(nèi)出現(xiàn)這種 OutOfMemoryError,只需刪除/注釋這一行即可:
To avoid this OutOfMemoryError inside the function body it's enough to remove/comment only this one line:
for (String string : strings);
<--這是邪惡的?。?!
for (String string : strings);
<-- this is the evil!!!
然后輸出如下:
在第一次分配內(nèi)存之前:1251k of max 176640k
Before first memory allocating: 1251k of max 176640k
第一次內(nèi)存分配后:最大 176640k 中的 131409k
After first memory allocation: 131409k of max 176640k
迭代后:131409k of max 176640k
After iteration: 131409k of max 176640k
在方法體中強制GC后:497k of max 176640k(垃圾被收集了!)
After force GC in the method body: 497k of max 176640k (the garbage is collected!)
再次嘗試在方法體中分配內(nèi)存:
Try to allocate memory in the method body again:
二級內(nèi)存分配后:115541k of max 163840k
After secondary memory allocation: 115541k of max 163840k
方法體后的GC:493k of max 163840k(垃圾被收集了!)
GC after the method body: 493k of max 163840k (the garbage is collected!)
方法外第三次分配總是成功:121300k of max 163840k
Third allocation outside the method is always successful: 121300k of max 163840k
所以,在沒有for迭代的情況下,在丟棄對字符串的引用后成功收集垃圾,并第二次分配(在函數(shù)體內(nèi))和第三次分配(在方法外).
So, without for iterating the garbage successfully collected after discarding the reference to the strings, and allocated second time (inside the function body) and allocated third time (outside the method).
我的假設:
for 語法構造被編譯成
Iterator iter = strings.iterator();
while(iter.hasNext()){
iter.next()
}
(我檢查了這個反編譯 javap -c IteratorAndGc.class
)
(and i checked this decompiling javap -c IteratorAndGc.class
)
并且看起來這個 iter 引用一直停留在范圍內(nèi).您無權訪問引用以使其無效,并且 GC 無法執(zhí)行收集.
And looks like this iter reference stays in the scope till the end. You don't have access to the reference to nullify it, and GC can't perform the collection.
也許這是正常行為(甚至可能在 javac 中指定,但我還沒有找到),但恕我直言,如果編譯器創(chuàng)建了一些實例,它應該關心在之后從范圍中丟棄它們使用.
Maybe this is normal behavior (maybe even specified in javac, but i haven't found), but IMHO if compiler creates some instances it should care about discarding them from the scope after using.
這就是我期望實現(xiàn) for
語句的方式:
That's how i expect to have the implementation of for
statement:
Iterator iter = strings.iterator();
while(iter.hasNext()){
iter.next()
}
iter = null; // <--- flush the water!
使用的 java 編譯器和運行時版本:
Used java compiler and runtime versions:
javac 1.8.0_111
java version "1.8.0_111"
Java(TM) SE Runtime Environment (build 1.8.0_111-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.111-b14, mixed mode)
注意:
問題不在于編程風格、最佳實踐、約定等等,問題是關于Java的效率平臺.
the question is not about programming style, best practices, conventions and so on, the question is about an efficiency of Java platform.
問題不在于 System.gc()
行為(您可以刪除所有gc 示例中的調(diào)用) - 在第二次字符串分配期間,JVM 必須 釋放丟棄的內(nèi)存.
the question is not about System.gc()
behavior (you may remove all
gc calls from the example) - during the second strings allocation the JVM must release the dicarded memory.
對測試java類的引用, 在線編譯器測試(但是這個資源只有50Mb的堆,所以使用N = 5000)
Reference to the test java class, Online compiler to test (but this resource has only 50 Mb of heap, so use N = 5000)
推薦答案
感謝您的錯誤報告.我們已修復此錯誤,請參閱 JDK-8175883.正如這里在 enhanced for 的情況下所評論的,javac 正在生成合成變量,因此對于如下代碼:
Thanks for the bug report. We have fixed this bug, see JDK-8175883. As commented here in the case of the enhanced for, javac was generating synthetic variables so for a code like:
void foo(String[] data) {
for (String s : data);
}
javac 大約在生成:
javac was approximately generating:
for (String[] arr$ = data, len$ = arr$.length, i$ = 0; i$ < len$; ++i$) {
String s = arr$[i$];
}
如上所述,這種轉換方法意味著合成變量 arr$ 持有對數(shù)組 data 的引用,一旦未引用該數(shù)組,就會阻止 GC 收集該數(shù)組不再在方法內(nèi)部.此錯誤已通過生成此代碼修復:
as mentioned above this translation approach implies that the synthetic variable arr$ holds a reference to the array data that impedes the GC to collect the array once it is not referred anymore inside the method. This bug has been fixed by generating this code:
String[] arr$ = data;
String s;
for (int len$ = arr$.length, i$ = 0; i$ < len$; ++i$) {
s = arr$[i$];
}
arr$ = null;
s = null;
這個想法是將 javac 創(chuàng)建的任何引用類型的合成變量設置為 null 以轉換循環(huán).如果我們談論的是原始類型的數(shù)組,那么最后一個對 null 的賦值不是由編譯器生成的.該錯誤已在 repo JDK repo
The idea is to set to null any synthetic variable of a reference type created by javac to translate the loop. If we were talking about an array of a primitive type, then the last assignment to null is not generated by the compiler. The bug has been fixed in repo JDK repo
這篇關于Java“為"語句實現(xiàn)防止垃圾收集的文章就介紹到這了,希望我們推薦的答案對大家有所幫助,也希望大家多多支持html5模板網(wǎng)!