Java 的for语句实现防止垃圾回收。

11 浏览
0 Comments

Java 的for语句实现防止垃圾回收。

更新 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 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 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 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 generateLargeStringsArray(int N) {
        ArrayList 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

在方法体中的强制垃圾回收之后:110682k of max 176640k(几乎没有回收)

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

     !!!! Out of memory error !!!!:     168948k of max     176640k

方法体之后的垃圾回收:459k of max 176640k(Garbage被收集了!)

方法外的第三次分配始终成功:117740k of max 163840k

在gcInMethod()函数内部,我尝试分配一个列表,对它进行迭代,丢弃对该列表的引用,(可选地)强制进行垃圾回收,并再次分配相似的列表。但由于内存不足,我无法分配第二个数组。

同时,在函数体外部,我可以成功地强制进行垃圾回收(可选),并再次分配相同的数组大小!

为了避免函数体内的OutOfMemoryError,只需删除或注释掉这一行:

for (String string : strings); <--这是罪魁祸首!!!

然后输出就会像这样:

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

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

迭代后:131409k最大176640k

在方法体内部强制GC后:497k最大176640k(垃圾被收集!)

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

在二次内存分配后:115541k最大163840k

在方法体后GC:493k最大163840k(垃圾被收集!)

方法外的第三次分配总是成功的:121300k最大163840k

因此,在迭代丢弃strings的引用后,成功收集垃圾,并在函数体内部分配第二次()和第三次分配(方法外)。

我的假设:

加利福尼亚州语法结构被编译为

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 必须释放已丢弃的内存。

测试 Java 类的参考在线编译器进行测试(但此资源只有 50 MB 的堆,因此请使用 N = 5000)

admin 更改状态以发布 2023年5月21日
0
0 Comments

所以这实际上是一个有趣的问题,可能会受益于稍微改变措辞。更具体地说,如果重点放在生成的字节码上,将会消除许多混淆。所以让我们这样做。

在给定的代码中:

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

这是生成的字节码:

   0: new           #2                  // class java/util/ArrayList
   3: dup           
   4: invokespecial #3                  // Method java/util/ArrayList."":()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 foo = new ArrayList<>();
for (Iterator i = foo.iterator(); i.hasNext(); ) {
  Integer val = i.next();
}

仍然没有清理。为什么呢?

然后,我们就在猜测的领域了,除非在JVM规范中明确指定(没有检查)。无论如何,要进行清理,编译器都必须为每个超出范围的变量生成额外的字节码(2条指令,aconst_nullastore_)。这意味着代码运行会变慢;为了避免这种情况,可能需要为JIT添加复杂的优化。

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

您最终会遇到与上述类似的情况。迭代器被分配并存储在本地变量1中。然后您的代码尝试分配新的字符串数组,并且由于本地变量1不再使用,它将存储在相同的本地变量中(检查字节码)。但是分配发生在赋值之前,因此仍然存在对迭代器的引用,因此没有内存。

如果在try块之前添加此行,则即使删除System.gc()调用,也可以正常工作:

int i = 0;

所以,似乎JVM开发人员做出了选择(生成更小/更高效的字节码,而不是明确地将超出范围的变量设置为null),而您编写的代码不适合他们对人们如何编写代码的假设。鉴于我从未在实际应用程序中遇到过这个问题,对我来说似乎是一件小事。

0
0 Comments

感谢您提供这个错误报告。我们已经修复了这个错误,请查看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$持有对数组data的引用,这妨碍了 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 repo 中得到修复。

0