更新于2017年11月21日:JDK已经修复了这个错误,请参考Vicente Romero的评论
摘要:
如果对于任何Iterable
实现使用for
语句,该集合将保留在堆内存中,直到当前作用域(方法、语句体)结束,即使您没有任何其他引用到该集合,应用程序需要分配新的内存也不会被垃圾回收。
http://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8175883
https://bugs.openjdk.java.net/browse/JDK-8175883
示例:
如果我有下面的代码,它分配了一个具有随机内容的大字符串列表:
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("\t%40s: %10dk of max %10dk%n", action, used / 1024, max / 1024);
}
}
使用有限的内存(例如180mb)编译并运行它,就像这样:
javac IteratorAndGc.java && java -Xms180m -Xmx180m IteratorAndGc
在运行时我有:
因此,在gcInMethod()内部,我尝试分配列表,迭代它,丢弃对列表的引用,(可选)强制垃圾回收并再次分配类似的列表。但是由于缺乏内存,我无法分配第二个数组。Before first memory allocating: 1251k of max 176640k
After first memory allocation: 131426k of max 176640k
After iteration: 131426k of max 176640k
After force GC in the method body: 110682k of max 176640k (almost nothing collected)
Try to allocate memory in the method body again:
!!!! Out of memory error !!!!: 168948k of max 176640k
GC after the method body: 459k of max 176640k (the garbage is collected!)
Third allocation outside the method is always successful: 117740k of max 163840k
同时,在函数体外,我可以成功地强制进行垃圾回收(可选),并再次分配相同大小的数组!
为了避免函数体内的OutOfMemoryError,只需删除/注释掉这一行:
for (String string : strings);
<-- 这就是罪魁祸首!!!
然后输出如下:
在第一次内存分配之前:1251k of max 176640k
第一次内存分配后:131409k of max 176640k
迭代后:131409k of max 176640k
在方法体中强制GC后:497k of max 176640k(垃圾已被清除!)
尝试在方法体中再次分配内存:
在第二次内存分配之后:115541k of max 163840k
方法体后的GC:493k of max 163840k(垃圾已被清除!)
方法外的第三次分配总是成功的:121300k of max 163840k
因此,如果没有for迭代,在丢弃对字符串的引用后,垃圾可以成功收集,并在第二次(函数体内)和第三次(方法外部)分配。
我的假设:
for语法结构编译为
Iterator iter = strings.iterator();
while(iter.hasNext()){
iter.next()
}
(and i checked this decompiling
javap -c IteratorAndGc.class
)并且我通过反编译
javap -c IteratorAndGc.class
进行了检查。而且看起来这个 iter 引用一直保持在作用域内,你无法访问引用以将其置为null,GC也无法进行回收。
也许这是正常的行为(甚至可能在 javac 中指定),但我认为如果编译器创建了一些实例,它应该关心在使用后将它们从作用域中丢弃。
这就是我希望
for
语句的实现方式:Iterator iter = strings.iterator();
while(iter.hasNext()){
iter.next()
}
iter = null; // <--- flush the water!
使用的Java编译器和运行时版本:
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平台的效率。
该问题与
System.gc()
的行为无关(您可以从示例中删除所有gc调用)-在第二个字符串分配期间,JVM 必须释放已丢弃的内存。