rad*_*tao 16 java garbage-collection iterator for-loop memory-management
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);
}
}
Run Code Online (Sandbox Code Playgroud)
用有限的内存编译和运行它,像这样(180mb):
javac IteratorAndGc.java && java -Xms180m -Xmx180m IteratorAndGc
Run Code Online (Sandbox Code Playgroud)
在运行时我有:
在第一次分配内存之前:最大176640k的1251k
第一次内存分配后:最大176640k的131426k
迭代后:最大176640k的131426k
在方法体中强制GC后:最大176640k的110682k(几乎没有收集)
尝试再次在方法体中分配内存:
Run Code Online (Sandbox Code Playgroud)!!!! Out of memory error !!!!: 168948k of max 176640k
GC方法体后:最大176640k 459k(收集垃圾!)
方法外的第三次分配总是成功:最大163840k的117740k
所以,在gcInMethod()里面我尝试分配列表,迭代它,丢弃对列表的引用,(可选)强制垃圾收集并再次分配类似的列表.但由于内存不足,我无法分配第二个数组.
同时,在函数体之外我可以成功强制垃圾收集(可选)并再次分配相同的数组大小!
为了避免在函数体内部出现这种OutOfMemoryError,只需删除/注释这一行:
for (String string : strings);
< - 这是邪恶的!
然后输出如下:
在第一次分配内存之前:最大176640k的1251k
第一次内存分配后:最大176640k的131409k
迭代后:最大176640k的131409k
在方法体中强制GC后:最大176640k的497k(垃圾被收集!)
尝试再次在方法体中分配内存:
二次内存分配后:最大163840k的115541k
GC方法体后:最大163840k 493k(收集垃圾!)
方法外的第三次分配总是成功的:最大163840k的121300k
所以,无需用于迭代丢弃参考串后成功地收集的垃圾,和分配的第二时间(函数体内)和分配的第三次(该方法之外).
我的假设:
用于编译语法构造
Iterator iter = strings.iterator();
while(iter.hasNext()){
iter.next()
}
Run Code Online (Sandbox Code Playgroud)
(我检查了这个反编译javap -c IteratorAndGc.class
)
并且看起来像这样的iter引用保持在范围直到结束.您无权访问该引用以使其无效,并且GC无法执行该集合.
也许这是正常的行为(甚至可能在javac中指定,但我还没有找到),但恕我直言,如果编译器创建了一些实例,它应该关心在使用后将它们从范围中丢弃.
这就是我期望实现for
声明的方式:
Iterator iter = strings.iterator();
while(iter.hasNext()){
iter.next()
}
iter = null; // <--- flush the water!
Run Code Online (Sandbox Code Playgroud)
使用的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)
Run Code Online (Sandbox Code Playgroud)
注意:
问题不在于编程风格,最佳实践,约定等等,问题在于Java平台的效率.
问题不在于System.gc()
行为(您可以从示例中删除所有
gc调用) - 在第二个字符串分配期间,JVM 必须释放被控制的内存.
Vic*_*ero 11
感谢错误报告.我们已修复此错误,请参阅JDK-8175883.正如在增强的for的情况下这里评论的那样,javac正在生成合成变量,因此代码如下:
void foo(String[] data) {
for (String s : data);
}
Run Code Online (Sandbox Code Playgroud)
javac近似产生:
for (String[] arr$ = data, len$ = arr$.length, i$ = 0; i$ < len$; ++i$) {
String s = arr$[i$];
}
Run Code Online (Sandbox Code Playgroud)
如上所述,这种转换方法意味着合成变量arr $保存对数组数据的引用,该引用阻止GC在方法内部不再引用时收集数组.通过生成此代码修复了此错误:
String[] arr$ = data;
String s;
for (int len$ = arr$.length, i$ = 0; i$ < len$; ++i$) {
s = arr$[i$];
}
arr$ = null;
s = null;
Run Code Online (Sandbox Code Playgroud)
我们的想法是将由javac创建的引用类型的任何合成变量设置为null以转换循环.如果我们讨论的是基本类型的数组,那么编译器不会生成对null的最后一次赋值.该错误已在repo JDK repo中修复
这里,增强的for语句的唯一相关部分是对象的额外本地引用.
你的例子可以简化为
public class Example {
private static final int length = (int) (Runtime.getRuntime().maxMemory() * 0.8);
public static void main(String[] args) {
byte[] data = new byte[length];
Object ref = data; // this is the effect of your "foreach loop"
data = null;
// ref = null; // uncommenting this also makes this complete successfully
byte[] data2 = new byte[length];
}
}
Run Code Online (Sandbox Code Playgroud)
这个程序也将失败OutOfMemoryError
.如果删除ref
声明(及其初始化),它将成功完成.
您需要了解的第一件事是范围与垃圾收集无关.Scope是一个编译时概念,它定义程序源代码中的标识符和名称可用于引用程序实体的位置.
垃圾收集由可达性驱动.如果JVM可以确定任何活动线程的任何潜在持续计算都无法访问对象,那么它将认为它有资格进行垃圾回收.此外,它System.gc()
是无用的,因为如果JVM找不到分配新对象的空间,它将执行主要集合.
所以问题就变成了:如果我们将它存储在第二个局部变量中,为什么JVM不能确定该byte[]
对象不再被访问?
我没有答案.在这方面,不同的垃圾收集算法(和JVM)可能表现不同.当局部变量表中的第二个条目具有对该对象的引用时,似乎此JVM不会将该对象标记为无法访问.
这是一个不同的场景,其中JVM的行为与您在迁移时对垃圾收集的预期完全不同:
所以这实际上是一个有趣的问题,可以从略有不同的措辞中受益。更具体地说,专注于生成的字节码会消除很多混乱。所以让我们这样做。
鉴于此代码:
List<Integer> foo = new ArrayList<>();
for (Integer i : foo) {
// nothing
}
Run Code Online (Sandbox Code Playgroud)
这是生成的字节码:
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: invokeinterface #4, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
14: astore_2
15: aload_2
16: invokeinterface #5, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
21: ifeq 37
24: aload_2
25: invokeinterface #6, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
30: checkcast #7 // class java/lang/Integer
33: astore_3
34: goto 15
Run Code Online (Sandbox Code Playgroud)
所以,逐个游戏:
请注意,在循环之后,不会清除循环中使用的任何内容。这不限于迭代器:循环结束后,最后一个元素仍然存储在局部变量 3 中,即使代码中没有对它的引用。
所以在你说“那是错的,错的,错的”之前,让我们看看当我在上面的代码之后添加这段代码时会发生什么:
byte[] bar = new byte[0];
Run Code Online (Sandbox Code Playgroud)
你在循环后得到这个字节码:
37: iconst_0
38: newarray byte
40: astore_2
Run Code Online (Sandbox Code Playgroud)
哦,看那个。新声明的局部变量与迭代器存储在相同的“局部变量”中。所以现在对迭代器的引用消失了。
请注意,这与您假设的等效 Java 代码不同。生成完全相同的字节码的实际 Java 等效项是:
List<Integer> foo = new ArrayList<>();
for (Iterator<Integer> i = foo.iterator(); i.hasNext(); ) {
Integer val = i.next();
}
Run Code Online (Sandbox Code Playgroud)
而且仍然没有清理。为什么?
好吧,这里我们在猜测领域,除非它实际上在 JVM 规范中指定(尚未检查)。无论如何,要进行清理,编译器必须为超出范围的每个变量生成额外的字节码(2 条指令aconst_null
和astore_<n>
)。这意味着代码运行得更慢;为了避免这种情况,可能需要将复杂的优化添加到 JIT 中。
那么,为什么您的代码会失败?
您最终会遇到与上述类似的情况。迭代器被分配并存储在局部变量 1 中。然后您的代码尝试分配新的字符串数组,并且由于不再使用局部变量 1,它将被存储在同一个局部变量中(检查字节码)。但是分配发生在分配之前,所以仍然有对迭代器的引用,所以没有内存。
如果您在try
块之前添加这一行,即使您删除了System.gc()
调用,事情也会起作用:
int i = 0;
Run Code Online (Sandbox Code Playgroud)
因此,似乎 JVM 开发人员做出了选择(生成更小/更高效的字节码,而不是显式将超出范围的变量清空),并且您碰巧编写的代码在他们对人们如何做的假设下表现不佳写代码。鉴于我在实际应用中从未见过这个问题,对我来说似乎是一件小事。
归档时间: |
|
查看次数: |
2255 次 |
最近记录: |