【问题标题】:Garbage Collection in Java with Recursive Function带有递归函数的Java垃圾收集
【发布时间】:2015-03-03 02:01:39
【问题描述】:

我知道在常规循环的每次迭代中,对象变得不可访问并标记为垃圾回收。递归调用呢?比如:

public void doWork() {

    Object a = new Object();

    ....some work with a...

    this.sleep(60000);
    doWork();


  }

第一次递归中的对象(即“a”)是在第二次递归开始后标记为垃圾回收,还是需要显式标记为 null,因为外部函数由于递归而永远不会完成。

【问题讨论】:

    标签: java recursion garbage-collection


    【解决方案1】:

    在每次递归调用期间,局部变量(这里是引用“a”)被压入堆栈。局部变量是 GC 根。在第二次递归调用期间,新的引用被压入堆栈。但是,第一个引用仍然存在,因此该对象仍然可以访问,因此无法进行垃圾回收。

    因此,如果您希望将第一个创建的对象标记为垃圾回收(当函数尚未完成时),您应该将“a”显式设置为 null。

    这是一个了解 GC 的有用链接:http://javabook.compuware.com/content/memory/how-garbage-collection-works.aspx

    【讨论】:

    • 根据可达性的定义,一旦执行超过变量的最后一次读取,由局部变量引用的对象就变得不可访问。
    • @MarkoTopolnik - 你的意思是说如果我写入一个对象而不稍后读取它的值,那么写入将不会完成(如果没有线程在读取它的值之后进行优化书面)
    • @TheLostMind 不是对象,但如果局部变量是唯一保存对对象的引用的地方,并且如果方法执行超过变量的最后一个点被读取,则该对象可能被认为不可访问。当然,不能保证运行时会真正收集它。
    • @MarkoTopolnik - 好的。谢谢:)
    【解决方案2】:

    我发现如果不再需要引用对象的局部变量会被垃圾回收。但是,这在调试时并不适用。我猜调试器会保留对它的引用,但在常规执行中不需要它。

    尝试执行以下代码,看看会发生什么。

    Object o1 = new Object();
    System.out.println(o1);
    WeakReference<Object> wr = new WeakReference<Object>(o1);
    System.gc();
    System.out.println(wr.get());
    

    我的输出(没有调试器):

    java.lang.Object@20662d1
    null
    

    我的输出(带有调试器):

    java.lang.Object@206b4fc
    java.lang.Object@206b4fc
    

    因此,即使本地方法仍在堆栈上,早期的引用也会被垃圾回收。

    【讨论】:

      【解决方案3】:

      第一次递归中的对象(即“a”)是在第二次递归开始后标记为垃圾回收,还是需要显式标记为 null,因为外部函数由于递归而永远不会完成。

      答案是……它取决于平台。

      变量a 仍然在作用域内,直到doWork() 的声明实例返回。另一方面,对我们来说很明显,当我们到达递归调用时,a 变量无法影响计算,并且(理论上)这意味着 GC 不再需要出于以下目的考虑它确定可达性。

      因此,归结为 JVM 是否足够聪明,能够意识到 a 不再重要。那是平台相关的。正如另一个答案所指出的,在附加调试器的情况下运行可以改变这一点。


      另一个答案提到尾调用优化。当然,如果 JVM 确实实现了这种优化,那么“旧”a 在概念上将在优化的“调用”中被“新”替换。但是,没有 HotSpot JVM(至少在 Java 8 之前)实现尾调用优化,因为优化会干扰依赖于能够计算堆栈帧的 Java 代码;例如安全码。

      但是,如果此特定代码 是一个重大的存储泄漏,那么它很可能没有实际意义,因为(缺少尾调用优化)该代码可能会在之前给你一个 StackOverflowError它填满了堆。

      【讨论】:

        【解决方案4】:

        您应该假设只要方法本身在调用堆栈中,所有本地引用都是有效的。这意味着您每次递归都会获得一个对 a 的活动引用。

        运行时可能会发现,当不再访问变量 a 的插槽时,它可能会被覆盖。在这种情况下,即使 doWork() 尚未返回,它也变得无法访问。但是,您不能依赖此。在最后一次使用 a 之后,a = null; 会有所帮助。

        【讨论】:

          【解决方案5】:

          理论上,递归是通过将实际帧(局部变量集)压入堆栈并为新的执行打开一个新帧来完成的。

          堆中的对象仍然被压入堆栈的帧中的变量引用,这意味着对于 GC,该对象仍然是“活动的”。

          但是,许多编译器(我也假设为 javac)能够识别和展开示例中的尾递归,从而大大简化了递归并释放了许多资源。

          然而,一般来说,递归会占用大量堆栈和堆空间,这就是为什么使用递归并不总是一个好主意。

          【讨论】:

          • 我从未见过 JVM 上的尾调用优化的任何证据。事实上,我很确定它不存在。
          • 嗯,这是非常罕见的情况,所以在真正的编译器中可能没有意义。不过,我很确定开启了最大优化设置的 gcc 会展开某种形式的尾递归。
          • gcc 是一个完全不同的世界......想想如何在 TCO 下保留线程堆栈跟踪。
          【解决方案6】:

          如果您想进行垃圾回收,通过将变量设置为null,这是一种主动摆脱引用的好方法。但是,不要忘记对象可能包含事件注册、单独的线程等等,所以如果你想对一个对象进行垃圾收集,你必须确保它们的引用没有泄漏。 Object 不是这种情况,但其他类的实例可能会发生这种情况。至于这个问题,我相信除非你主动摆脱它的引用,否则它不会被垃圾收集,但我还没有在所有 Java 版本上测试过。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2010-12-13
            • 2015-12-21
            • 1970-01-01
            • 1970-01-01
            • 2021-04-15
            • 2011-11-07
            • 2011-02-25
            相关资源
            最近更新 更多