System.gc()会回收仍然被本地变量引用的对象

3
当我运行以下程序时:
public static void main(String[] args) {

    ArrayList<Object> lists = new ArrayList<>();
    for (int i = 0; i <200000 ; i++) {
        lists.add(new Object());
    }
    System.gc();
    try {
        Thread.sleep(Integer.MAX_VALUE);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

我会转储堆

jmap -dump:live,format=b,file=heap.bin 27648
jhat -J-Xmx2G heap.bin

ArrayList 和 200000 个对象丢失了。

我不知道为什么 JVM 知道这些对象将不会被使用,也不知道为什么 JVM 判断这个 GC 根不是一个引用。


1
你的问题是什么?200000个对象并不是丢失了,它从来就不存在。如果你想要实例化它,那么不要写成<200000,而应该写成<=200000。如果你的问题是:为什么垃圾回收器没有运行,那么...我们可以请求垃圾回收器运行,但我们不能自己让它运行。 - Stultuske
谢谢,你可以试一下,我在localhost:7000中找不到200000个对象。 - K.shun
我刚刚解释了为什么你的代码没有创建一个。 - Stultuske
2
@Stultuske,OP并没有询问第200000个对象,而是所有200000个对象。当然,他的语法有待改进。但是,你的解释完全错误。当您从零开始迭代条件<n时,您将执行n次迭代,因此在每次迭代中创建一个对象时创建n个对象。这是标准的for循环习惯用法,在CJava和许多其他语言中使用。不要建议在此处使用<=,因为那样会多进行一次迭代 - Holger
1个回答

6
本地变量本身并不是GC根。 Java®语言规范 定义如下:
“可达对象”是指可以从任何活动线程中的任何潜在连续计算中访问的任何对象。
显然,需要一个持有对象引用的变量才能使它能够在“潜在的连续计算”中从活动线程中访问,因此,缺乏这样的变量可以用作检查对象是否不可访问的易于检查的标志。
但这并不排除额外努力来识别仍由本地变量引用的不可访问对象。规范甚至在同一节中明确声明:
“程序的优化转换可以被设计为减少可达对象的数量,使其小于那些可能被认为是可达的对象的数量。例如,Java编译器或代码生成器可以选择将不再使用的变量或参数设置为null,以使该对象的存储能够更快地被回收。”

无论它是否实际执行取决于当前执行模式,例如方法是解释运行还是已经编译。从Java 9开始,您可以插入显式屏障,例如:
public static void main(String[] args) {
    ArrayList<Object> list = new ArrayList<>();
    for (int i = 0; i <200000 ; i++) {
        list.add(new Object());
    }
    System.gc();
    try {
        Thread.sleep(Integer.MAX_VALUE);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    Reference.reachabilityFence(list);
}

这将强制列表保持活动状态。
对于早期的Java版本,另一种选择是同步:
public static void main(String[] args) {
    ArrayList<Object> list = new ArrayList<>();
    for (int i = 0; i <200000 ; i++) {
        list.add(new Object());
    }
    System.gc();
    synchronized(list) {
        try {
            Thread.sleep(Integer.MAX_VALUE);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

通常情况下,您希望未使用的对象尽早被收集。当您将finalize()与对可达性的天真假设一起使用时,可能会出现问题。


1
虽然答案提供了关于可达性等方面的良好理论背景,但是解释为什么ArrayList在这种特殊情况下被收集的原因并不完全正确。jmap没有进行“额外的努力”。live选项只是调用与System.gc相同的GC周期 - 在这种情况下,即使没有live,列表也将被收集。由于程序具有长时间运行的循环,main方法被JIT编译,并且在OSR替换之后,不再有保存对列表引用的堆栈帧局部变量。 - apangin
1
@apangin:在这方面,OSR存在其局限性,特别是当循环创建对象仍在运行时。我验证了System.gc()调用在这里没有效果,例如通过监视使用的堆并改变触发堆转储的时间。当然,“额外的努力”包括触发垃圾回收并使OSR工作,但它只在这里起作用,因为main线程处于sleep状态以支持堆转储。例如,当您将sleep替换为while(true);循环时,对象不再被收集。(使用jdk1.8.0_65测试) - Holger
"System.gc()调用在此处无效" - 实际上是有效的。我不确定您是如何验证的,但堆快照(使用jmap -F拍摄)不包含任何大型ArrayLists。 - apangin
while (true); 防止在更高层次上编译。 -XX:+PrintCompilation 将显示类似于“COMPILE SKIPPED: trivial infinite loop (retry at different tier)”的内容。这就是为什么在这种情况下不收集列表的原因。 - apangin
1
正如@apangin所说,我监视了堆,当调用System.gc()时,看不到任何显著的影响,但在获取堆转储时使用的内存有显著下降(在不同运行中时间有所变化)。现在,可以肯定的是,可能有其他对象支配着使用的堆,在第一个System.gc()中没有被收集,但在获取堆转储时被收集了。顺便说一句,即使从sleep中删除try catch,并在主方法中声明throws InterruptedException,结果也会改变。 - Holger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接