Java“for”语句实现可防止垃圾回收

问题描述 投票:14回答:6

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

在运行时我有:

在第一次分配内存之前:最大176640k的1251k

第一次内存分配后:最大176640k的131426k

迭代后:最大176640k的131426k

在方法体中强制GC后:最大176640k的110682k(几乎没有收集)

尝试再次在方法体中分配内存:

     !!!! 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()
}

(我检查了这个反编译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 classOnline compiler to test(但这个资源只有50 Mb的堆,所以使用N = 5000)

java for-loop memory-management garbage-collection iterator
6个回答
8
投票

感谢错误报告。我们已修复此错误,请参阅JDK-8175883。正如在增强的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的最后一次赋值。该错误已在repo JDK repo中修复


5
投票

这里,增强的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];
    }
}

这个程序也会因OutOfMemoryError而失败。如果删除ref声明(及其初始化),它将成功完成。

您需要了解的第一件事是范围与垃圾收集无关。 Scope is a compile time concept that defines where identifiers and names in a program's source code can be used to refer to program entities.

Garbage collection is driven by reachability.如果JVM可以确定任何活动线程的任何潜在持续计算都无法访问对象,那么它将认为它有资格进行垃圾收集。此外,System.gc()是无用的,因为如果找不到分配新对象的空间,JVM将执行主要集合。

所以问题变成了:如果我们将它存储在第二个局部变量中,为什么JVM不能确定不再访问byte[]对象?

我没有答案。在这方面,不同的垃圾收集算法(和JVM)可能表现不同。当局部变量表中的第二个条目具有对该对象的引用时,似乎此JVM不会将该对象标记为无法访问。


这是一个不同的场景,其中JVM的行为与您在垃圾收集方面的预期完全不同:


4
投票

所以这实际上是一个有趣的问题,可能会从略有不同的措辞中受益。更具体地说,专注于生成的字节码反而会清除很多混乱。所以,让我们这样做。

鉴于此代码:

List<Integer> foo = new ArrayList<>();
for (Integer i : foo) {
  // nothing
}

这是生成的字节码:

   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

所以,玩游戏:

  • 将新列表存储在局部变量1(“foo”)中
  • 将迭代器存储在局部变量2中
  • 对于每个元素,将元素存储在局部变量3中

请注意,在循环之后,不会清除循环中使用的任何内容。这不仅限于迭代器:在循环结束后,最后一个元素仍然存储在局部变量3中,即使代码中没有对它的引用。

所以在你说“那是错的,错的,错的”之前,让我们看看当我在上面的代码之后添加这段代码时会发生什么:

byte[] bar = new byte[0];

循环后得到这个字节码:

  37: iconst_0      
  38: newarray       byte
  40: astore_2      

哦,看那个。新声明的局部变量存储在与迭代器相同的“局部变量”中。所以现在对迭代器的引用已经消失了。

请注意,这与您假设的等效Java代码不同。实际的Java等价物生成完全相同的字节码,如下所示:

List<Integer> foo = new ArrayList<>();
for (Iterator<Integer> i = foo.iterator(); i.hasNext(); ) {
  Integer val = i.next();
}

而且还没有清理。为什么?

好吧,这里我们在猜测领域,除非它实际上是在JVM规范中指定的(尚未检查)。无论如何,要进行清理,编译器必须为每个超出范围的变量生成额外的字节码(2条指令,aconst_nullastore_<n>)。这意味着代码运行速度较慢;为了避免这种情况,可能需要将复杂的优化添加到JIT中。

那么,为什么你的代码失败了?

您最终处于与上述类似的情况。迭代器被分配并存储在局部变量1中。然后你的代码尝试分配新的字符串数组,因为局部变量1不再使用,它​​将存储在同一个局部变量中(检查字节码)。但是分配发生在赋值之前,所以仍然有对迭代器的引用,所以没有内存。

如果你在try块之前添加这一行,即使你删除了System.gc()调用,事情仍然有效:

int i = 0;

因此,似乎JVM开发人员做出了选择(生成更小/更高效的字节码而不是显式地使变量超出范围),并且您碰巧编写的代码在人们如何做出的假设下表现不佳写代码。鉴于我在实际应用中从未见过这个问题,对我来说似乎是件小事。


3
投票

正如其他答案中已经说明的那样,变量范围的概念在运行时是未知的。在已编译的类文件中,局部变量仅位于堆栈帧(由索引寻址)内,执行写入和读取。如果多个变量具有析取范围,则它们可以使用相同的索引,但是没有正式声明它们。只有写入新值才会丢弃旧值。

因此,有三种方法,如何将本地变量存储中的引用视为未使用:

  1. 存储位置被新值覆盖
  2. 该方法退出
  3. 没有后续代码读取该值

显而易见的是,第三点是最难检查的,因此,它并不总是适用,但是当优化器开始工作时,它可能会导致另一方面的意外,如“Can java finalize an object when it is still in scope?”和“finalize() called on strongly reachable object in Java 8”中所述。 。

在您的情况下,应用程序很快就会运行并且可能是非优化的,这可能导致由于第3点和第1点和第2点不适用而导致引用未被识别为未使用。

您可以轻松验证是否是这种情况。当你改变线

ArrayList<String> strings2 = generateLargeStringsArray(N);

ArrayList<String> strings2 = null;
strings2 = generateLargeStringsArray(N);

OutOfMemoryError消失了。原因是此时保存在前面的Iterator循环中使用的for的存储位置尚未被覆盖。新的局部变量strings2将重用存储,但这仅在实际写入新值时才会显示。因此,在调用null之前使用generateLargeStringsArray(N)初始化将覆盖Iterator引用并允许收集旧列表。

或者,您可以使用选项-Xcomp以原始形式运行程序。这迫使所有方法的编译。在我的机器上,它有明显的启动减速,但由于可变的使用分析,OutOfMemoryError也消失了。

让应用程序在初始化期间分配那么多内存(与最大堆大小相比),即大多数方法运行时解释,是一个不寻常的极端情况。通常,大多数热方法在内存消耗很高之前被充分编译。如果你在现实生活中反复遇到这个角落案例,那么-Xcomp可能适合你。


1
投票

最后,Oracle / Open JKD错误被接受,批准和修复:

http://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8175883

https://bugs.openjdk.java.net/browse/JDK-8175883

引用线程中的注释:

这是一个在8和9都可重现的问题

有一些问题,程序会保留它自己对内存块的隐式自动生成引用,直到下一次隐式使用并且其内存被锁定而导致OOM

(这证明@vanza's expectation,见this example from the JDK developer

根据规范,这不应该发生

(这是我的问题的答案:如果编译器创建了一些实例,它应该关心在使用后将它们从范围中丢弃)

UPD 21.11.2017:错误在JDK中修复,请参阅comment from Vicente Romero


0
投票

只是总结一下答案:

正如@ sotirios-delimanolis提到in his comment关于The enhanced for statement - 我的假设是明确定义的:for sugar语句编译为IteratorhasNext()-next()调用:

#i是一个自动生成的标识符,它与发生增强for语句时的范围(§6.3)中的任何其他标识符(自动生成的或其他标识符)不同。

就像那时@vanza showed in his answer:这个自动生成的标识符可能会或者可能不会在以后被覆盖。如果被覆盖 - 内存可能被释放,如果没有 - 内存不再被释放。

仍然(对我而言)是一个悬而未决的问题:如果Java编译器或JVM创建了一些隐式引用,那么稍后它是否应该关注丢弃这些引用呢?是否可以保证在下一次内存分配之前的下一次调用中将重用相同的自动生成的迭代器引用?不应该是一个规则:那些分配记忆然后关心释放它的人?我会说 - 它必须关心这一点。否则行为是未定义的(它可能会落到OutOfMemoryError,或者可能不会 - 谁知道......)

是的,我的例子是一个极端情况(没有在for迭代器和下一个内存分配之间初始化),但这并不意味着它是不可能的情况。这并不意味着这种情况很难实现 - 很可能在有限的内存环境中使用一些大数据并立即重新分配内存。我在我的工作应用程序中找到了这种情况,我解析了一个大的XML,它“占用”超过一半的内存。

(问题不仅仅是迭代器和for循环,猜测它是常见的问题:编译器或JVM有时不会清理自己的隐式引用)。

© www.soinside.com 2019 - 2024. All rights reserved.