Java“ for”语句实现可防止垃圾收集
问题内容:
UPD 21.11.2017: 该错误已在JDK中修复,请参见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
在运行时,我有:
在第一次内存分配之前:最大176640k的1251k
第一次分配内存后:131426k,最大176640k
迭代后:131426k,最大176640k
在方法主体中强制装填GC之后:最大为176640k的110682k(几乎没有收集到任何东西)
尝试再次在方法主体中分配内存:
!!!! Out of memory error !!!!: 168948k of max 176640k
方法主体后的GC:最大459640k的459k(收集垃圾!)
方法外的第三次分配总是成功的:117740k,最大163840k
因此,在 gcInMethod()
内部,我尝试分配列表,对其进行迭代,丢弃对该列表的引用,(可选)强制垃圾回收并再次分配相似的列表。但是由于内存不足,我无法分配第二个数组。
同时,在函数体之外,我可以成功地强制进行垃圾回收(可选)并再次分配相同的数组大小!
为了避免函数体内出现此 OutOfMemoryError ,仅删除/注释这一行就足够了:
for (String string : strings);
<-这是邪恶!!!
然后输出如下所示:
在第一次内存分配之前:最大176640k的1251k
第一次分配内存后:131409k,最大176640k
迭代后:131409k,最大176640k
在方法主体中强制执行GC之后:最大176640k的497k(收集垃圾!)
尝试再次在方法主体中分配内存:
分配辅助内存后:最大115840k的115541k
方法主体后的GC:最大163840k的493k(垃圾收集!)
方法外的第三次分配始终成功:最高163840k的121300k
所以,无需 用于 迭代垃圾成功丢弃参考串后收集,并且分配的第二时间(函数体内)和分配的第三次(该方法之外)。
我的假设:
用于 语法构造的编译为
Iterator iter = strings.iterator();
while(iter.hasNext()){
iter.next()
}
(我检查了这个反编译javap -c IteratorAndGc.class
)
并且看起来像这样的 迭代 参考一直保留到最后。您无权访问引用以使其无效,并且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 必须 释放丢弃的内存。
问题答案:
增强的for所述 ,javac正在生成综合变量,因此对于如下代码:
void foo(String[] data) {
for (String s : data);
}
javac大约生成:
for (String[] arr$ = data, len$ = arr$.length, i$ = 0; i$ < len$; ++i$) {
String s = arr$[i$];
}
如上所述,这种转换方法意味着合成变量 arr $ 拥有对数组 数据
的引用,一旦该方法内部不再对其进行引用,该引用就会阻止GC收集数组。通过生成以下代码已修复了该错误:
String[] arr$ = data;
String s;
for (int len$ = arr$.length, i$ = 0; i$ < len$; ++i$) {
s = arr$[i$];
}
arr$ = null;
s = null;
想法是将javac创建的用于转换循环的引用类型的任何综合变量设置为null。如果我们在谈论原始类型的数组,则编译器不会生成对null的最后一个赋值。该错误已在回购JDK回购中修复