【问题标题】:Does JVM garbage collect objects being referenced by local variables which are no longer used? [duplicate]JVM 是否会收集不再使用的局部变量引用的对象? [复制]
【发布时间】:2019-03-01 03:22:50
【问题描述】:

据我所知,方法的局部变量位于执行线程中的堆栈帧中,并且局部变量的引用类型只有对象的引用,而不是对象本身。 JVM 中的所有对象都位于堆空间中。

我想知道正在执行的方法中由局部变量引用的对象在方法执行结束之前永远不会被垃圾收集。 (不使用 java.lang.ref.WeakReference 和 SoftReference。)

它们是垃圾收集的吗?还是从不?有编译器对这类东西的优化吗?

(如果它们从不被垃圾回收,这意味着在执行需要很长时间的大方法时,可能需要将 null 分配给不再使用的变量。)

【问题讨论】:

    标签: java garbage-collection jvm


    【解决方案1】:

    正如Can java finalize an object when it is still in scope? 中所述,局部变量不会阻止引用对象的垃圾回收。或者,正如this answer 所说,范围只是一个语言概念,与垃圾收集器无关。

    我将再次引用规范的相关部分,JLS §12.6.1

    可达对象是可以在任何潜在的持续计算中从任何活动线程访问的任何对象。

    此外,我将答案的示例扩展到

    class A {
        static volatile boolean finalized;
    
        Object b = new Object() {
            @Override protected void finalize() {
                System.out.println(this + " was finalized!");
                finalized = true;
            }
            @Override public String toString() {
                return  "B@"+Integer.toHexString(hashCode());
            }
        };
        @Override protected void finalize() {
            System.out.println(this + " was finalized!");
        }
    
        @Override public String toString() {
            return super.toString() + " with "+b;
        }
    
        public static void main(String[] args) {
            A a = new A();
            System.out.println("Created " + a);
            for(int i = 0; !finalized; i++) {
                if (i % 1_000_000 == 0)
                    System.gc();
            }
            System.out.println("finalized");
        }
    }
    
    Created A@59a6e353 with B@6aaa5eb0
    B@6aaa5eb0 was finalized!
    finalized
    A@59a6e353 with B@6aaa5eb0 was finalized!
    

    这表明即使是变量在范围内的方法也可以检测到引用对象的终结。此外,从堆变量中引用也不一定会阻止垃圾回收,因为 B 对象是不可访问的,因为当包含引用的对象也不可访问时,没有继续计算可以访问它。


    值得强调的是,即使使用对象并不总是会阻止其垃圾回收。重要的是,正在进行的操作是否需要对象的内存,而不是每次对源代码中对象字段的访问都必须在运行时导致实际的内存访问。规范规定:

    可以设计优化程序的转换,将可到达的对象的数量减少到比那些天真地认为是可到达的要少。 […]

    如果对象字段中的值存储在寄存器中,则会发生这种情况的另一个示例。然后程序可能会访问寄存器而不是对象,并且永远不会再次访问对象。这意味着该对象是垃圾。

    这不仅仅是理论上的选择。正如finalize() called on strongly reachable object in Java 8 中所讨论的,它甚至可能在对象上调用方法时发生,或者换句话说,this 引用可能会在实例方法仍在执行时被垃圾收集。

    确定防止对象垃圾回收的唯一方法是,如果终结器也对对象进行同步,则对对象进行同步或调用Reference.reachabilityFence(object),这是在 Java 9 中添加的一种方法。后来添加的栅栏方法演示了优化器在不同版本之间变得更好对早于想要的垃圾收集问题的影响。当然,首选的解决方案是编写完全不依赖垃圾回收时间的代码。

    【讨论】:

    • 我不明白这如何证明具有局部变量引用的对象已被收集。 A 的最终确定发生在 main() 打印“最终确定”之后,据我们所知,它也退出后,即当A 超出范围时,收集 (如果有)之后。 B 成员对象的最终确定更有趣。
    • @MarquisofLorne 在我的答案开头是指向this answer 的链接,它已经显示了局部变量引用的对象如何被垃圾收集。我只是扩展它以表明这反过来又允许堆变量引用的对象的集合。注意JLS §12.6.1明确禁止在A之前收集B;两者在这里收集在一起,但由于未指定终结顺序,JVM 恰好在 A 之前终结了 B。
    【解决方案2】:

    并非所有对象都在堆空间中。但这通常是正确的。 Java 已扩展为具有堆栈本地对象,前提是 JVM 可以检测到该对象将仅与堆栈帧一样长。

    现在是堆上的对象,它们在方法中具有本地引用。在处理方法时,与方法运行关联的堆栈帧包含局部变量引用。只要可以使用引用(包括仍在堆栈帧中),对象就不会被垃圾回收。

    一旦引用被销毁,并且正在运行的程序无法再访问该对象(因为没有可以访问它的引用),那么垃圾收集器将收集它。

    【讨论】:

    • 谢谢。从哪个版本的 java 具有堆栈本地对象功能?有什么我可以阅读的参考资料吗?
    • “堆栈本地对象”是一个顽固的神话。像 HotSpot JVM 这样的 JVM 具有“对象缩放”,这意味着对象会溶解;将没有对象头,并且字段将转换为受后续优化的变量,与局部变量相同的方式,即常量折叠和消除冗余或未使用的变量。其余变量通常映射到 CPU 寄存器。因此,如果没有足够的寄存器,剩余的一些字段可能在堆栈上结束,但结果与原始堆对象没有相似之处。
    • @Holger 感谢您指出这一点。我目前在阅读逃逸分析以及它如何影响 JVM(到目前为止似乎非常小)方面获得了很多乐趣。如果没有您的评论,我可能会错过今天早上获得一点学习乐趣的机会!
    • 请注意,对象不需要纯粹是本地的以启用早期垃圾收集。例如,一个方法可以接收一个对象,该对象保存在引用变量foo 中,并多次访问foo.x。优化后的代码可能只读取一次foo.x 并从现在开始使用x 的值,而不保留对foo 的引用。如果foo 是唯一的引用,则对象可能会在x 仍在使用时被垃圾回收。由于内联,如果使用是间接的,这甚至可能适用,例如通过在foo 上调用方法。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-05-26
    • 1970-01-01
    • 2021-11-22
    • 2012-12-20
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多