【发布时间】:2017-07-13 04:51:18
【问题描述】:
UPD 21.11.2017:该错误已在 JDK 中修复,请参阅 comment from Vicente Romero
总结:
如果for 语句用于任何Iterable 实现,则集合将保留在堆内存中直到当前范围(方法、语句主体)结束,即使您不这样做也不会被垃圾回收'没有对集合的任何其他引用,应用程序需要分配新内存。
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
在运行时我有:
在第一次分配内存之前:1251k of max 176640k
第一次内存分配后:131426k of max 176640k
迭代后:131426k of max 176640k
在方法体中强制 GC 后:最大 176640k 的 110682k(几乎没有收集到)
再次尝试在方法体中分配内存:
!!!! Out of memory error !!!!: 168948k of max 176640k方法体后的GC:459k of max 176640k(垃圾被收集了!)
方法外第三次分配总是成功:117740k of max 163840k
所以,在 gcInMethod() 内部,我尝试分配列表,对其进行迭代,丢弃对列表的引用,(可选)强制垃圾收集并再次分配类似的列表。但由于内存不足,我无法分配第二个数组。
同时,在函数体之外,我可以成功强制垃圾回收(可选)并再次分配相同的数组大小!
为了避免函数体内出现这种 OutOfMemoryError,只需删除/注释这一行即可:
for (String string : strings);
然后输出如下:
在第一次分配内存之前:1251k of max 176640k
第一次内存分配后:最大 176640k 中的 131409k
迭代后: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()
}
(我检查了这个反编译javap -c IteratorAndGc.class)
并且看起来这个 iter 引用一直停留在范围内。您无权访问引用以使其无效,并且 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 必须 释放丢弃的内存。
Reference to the test java class, Online compiler to test(但是这个资源只有 50 Mb 的堆,所以使用 N = 5000)
【问题讨论】:
-
你错误地理解了 GC 是如何工作的。不能保证 GC 会在一次调用后收集任何东西。
-
该死的,请阅读说明!不是关于 GC 调用的问题!!! “问题不在于 System.gc() 行为(您可以从示例中删除所有 gc 调用) - 在第二个字符串分配期间,JVM 必须释放被丢弃的内存。”问题是关于“for”实现。
-
您认为迭代器和相关的类加载应该占用 150Mb 的内存?猜不。此外,当我离开函数体时,这些迭代器类不会被删除,但内存会被释放!此外,类被加载到自己的内存中,而不是堆中。
-
相比什么? 以任何方式 遍历集合,您必须保留对它的引用。 “问题”在于您正在遍历集合,而不是任何东西的“实现”。注意“在第二次字符串分配期间,JVM 必须释放丢弃的内存”是错误的。请放下大胆的脸。它伤害了我的眼睛。
-
@EJP 内存不足!我还在问题中描述了编译器如何编译“for”语句 - 它编译为 iterator-while 结构。
标签: java for-loop memory-management garbage-collection iterator