当前位置: 首页 > 面试题库 >

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

王君墨
2023-03-14
问题内容

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

引用测试java类,在线编译器进行测试(但是此资源只有50
Mb的堆,因此使用N = 5000)


问题答案:

增强的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回购中修复



 类似资料:
  • UPD 21.11.2017:该bug在JDK中被修复,参见Vicente Romero的评论 摘要: 如果语句用于任何实现,则集合将保留在堆内存中,直到当前作用域(方法、语句体)结束,即使您没有对集合的任何其他引用,并且应用程序需要分配新内存,也不会被垃圾收集。 在运行时,我有: 第一次内存分配前:最大176640K的1251K 第一次内存分配后:最大176640K的131426K 为了避免在函

  • 问题内容: 如何防止对象收集垃圾? 是否有通过终结或幻影引用的方法或任何其他方法? 采访中有人问我这个问题。面试官建议可以使用。 问题答案: 保持参考。如果过早地收集了对象,则表明您的应用程序设计中存在错误。 垃圾收集器仅收集应用程序中没有引用的对象。如果没有可以自然引用所收集对象的对象,请问自己为什么要保持它的生命。 通常没有引用但想要保留一个对象的一个​​用例是单例。在这种情况下,您可以使用静

  • 本文向大家介绍Java垃圾收集,包括了Java垃圾收集的使用技巧和注意事项,需要的朋友参考一下 示例 C ++方法-新增和删除 在像C ++这样的语言中,应用程序负责管理动态分配的内存所使用的内存。当使用new运算符在C ++堆中创建对象时,需要相应地使用delete运算符来处置该对象: 如果程序忘记了delete一个对象而只是“忘记”了该对象,则关联的内存将丢失给应用程序。这种情况的术语是内存泄

  • 主要内容:1 什么是Java 垃圾回收,2 Java 垃圾回收的优势,3 如何取消对象引用,4 finalize()方法,5 gc()方法,6 Java 垃圾回收的例子1 什么是Java 垃圾回收 在Java中,垃圾意味着未引用的对象。 垃圾回收是自动回收运行时未使用的内存的过程。换句话说,这是销毁未使用对象的一种方法。 我们在C语言中使用free() 函数,在C ++中使用delete()。但是,在Java中它是自动执行的。因此,java提供了更好的内存管理。 2 Java 垃圾回收的优势 它

  • 问题内容: 我想知道Java中发生的垃圾回收。它真的能够处理所有未使用的对象并释放最大可能的内存吗? 我还想知道Java垃圾收集与另一种语言(例如C#)相比如何?然后,如何自动垃圾收集与从像C这样的语言中进行手动收集相比又能达到更好的效果呢? 问题答案: 是的,这就是垃圾收集的重点。 有许多不同形式的垃圾收集。如果不增强算法,最简单的形式即引用计数就无法处理某些类型的垃圾(循环引用)。 Java(

  • 示例: 如果有这样的事件序列呢? Java为A分配内存 运行构造函数。现在堆中有一个实例。 GC运行。 对象没有引用,在分配给之前将其删除。 它如何防止这种情况发生?我将非常感谢链接到文章的解释。