【问题标题】:Interview question: Objects eligible for garbage collection面试题:符合垃圾回收条件的对象
【发布时间】:2023-03-23 10:50:01
【问题描述】:

给出以下代码:

class A {
    Boolean b;
    A easyMethod(A a){
        a = null;
        return a;
    }
    public static void main(String [] args){
        A a1 = new A();
        A a2 = new A();
        A a3 = new A();
        a3 = a1.easyMethod(a2);
        a1 = null;
        // Some other code 
    }
}

问题是在// Some other code 之前有多少对象可以进行垃圾回收。

那么正确答案是(至少这是面试官的答案):2 - 布尔值 b,因为它是一个包装器和 a1

你能解释一下为什么a2a3 没有被垃圾收集吗?

稍后编辑:

  • 好的,我想我现在明白了。一开始有点困惑,但现在我确定面试官错了。我最初的错误是,一开始我没有考虑到 Java 只是按值传递,所以不可能从以“a2”作为参数的函数内部使 a2 为空,因为 a2 实际上是 a2 的副本。
  • 带有布尔b的部分确实很明显。

感谢您的回答,之后我会发送一些面试反馈:)。

【问题讨论】:

  • b 没有被垃圾回收。它根本没有设置。
  • 您没有显示go 方法。这是否意味着调用 easyMethod,而在此示例中从未调用过它?
  • 在我看来,进行采访的人甚至不明白给你的代码没有丝毫连贯性。把它交给你的编译器,玩得开心(这只是前言)。我希望你这次面试没有成功。在其他地方,你正陷入可怕的混乱之中。
  • 请解决你的问题,没有b ...
  • 说b是GC是没有意义的

标签: java garbage-collection


【解决方案1】:

提供a1.go(a2)实际上是a1.easyMethod(a2),答案确实是2,但不是你列出的那些。正如 Bozho 正确指出的那样,b 未初始化,因此它不引用任何对象。在评论处符合垃圾回收条件的两个对象是 a1a3 最初引用的对象。

a1 显然被清空了,a3 被重新赋值给a1.easyMethod(a2) 的返回值,即为空。但是,a2 不受方法调用的影响,如Java is pass by value,因此只有引用a2 的副本被传递给方法。即使副本设置为 null,也不会影响原始 a2 的值。

【讨论】:

  • +1 提到 Java 是按值传递的。它只是从不传递对象,只是它们的引用。我理解它为什么会让一些人感到困惑,但我认为这是每个 (Java) 开发人员都应该知道的。
  • -1 所有三个对象都无法访问,因此此时可以进行垃圾回收。
  • @JonHarrop,这完全是错误的。在// Some other code 之前,仍然可以通过引用a2 访问其中一个对象。我阅读了您对@Mark 回答的评论,指出原始问题的措辞不正确是正确的,即“有资格进行垃圾收集”不是一个精确的术语。但是,您在这里的评论显然是不正确的。
  • 同意@JonHarrop。如果 a2 未被 // 其他代码使用,则 a2 引用的对象确实有资格进行垃圾回收。许多运行时/GC 将使用有关变量的活跃度信息,并将未用于方法其余部分的变量视为“死”。这就是 .Net 框架 (msdn.microsoft.com/en-us/library/system.gc.keepalive.aspx) 中存在方法 GC.KeepAlive 的原因,以确保该方法不再使用的变量在调用 GC.KeepAlive 之前一直保持活动状态。
  • @joncham,确实 if a2 不会被// Some other code 使用,它所引用的对象有资格进行垃圾回收。所以可以提一下,有问题的对象可能有资格进行垃圾回收,这取决于我们不知道的因素(即// Some other code 会做什么)。但是,此时唯一肯定符合垃圾收集条件的对象是另外两个。此外,@JonHarrop 指出在// Some other code 之前无法访问有问题的对象显然是错误的,因为它可以通过a2 访问。
【解决方案2】:

假设 go 应该是 easyMethod 它是这样工作的

class A {
    Boolean b;
    A easyMethod(A a){
        a = null; // the reference to a2 was passed in, but is set to null
                  // a2 is not set to null - this copy of a reference is!
        return a; // null is returned
    }
    public static void main(String [] args){
        A a1 = new A(); // 1 obj
        A a2 = new A(); // 2 obj
        A a3 = new A(); // 3 obj
        a3 = a1.go(a2); // a3 set to null and flagged for GC - see above for why
        a1 = null; // so far, a1 and a3 have been set to null and flagged
        // Some other code 
    }
}

有两个对象可以进行垃圾回收(a1 和 a3)。 b 不是因为它只是对 null 的引用。从未制作过Boolean

为了解决// Some other code 可能存在的微妙之处,我将问题改写为以下内容:

预测并解释以下输出:

class A {
    int i;
    A(int i) { this.i = i; }
    public String toString() { return ""+i; }
    A go(A a){
        a = null; // the reference to a2 was passed in, but is set to null
                  // a2 is not set to null - this copy of a reference is!
        return a; // null is returned
    }
    public static void main(String [] args){
        A a1 = new A(1); // 1 obj
        A a2 = new A(2); // 2 obj
        A a3 = new A(3); // 3 obj
        a3 = a1.go(a2); // a3 set to null and flagged for GC - see above for why
        a1 = null; // so far, a1 and a3 have been set to null and flagged

        test(a1);
        test(a2);
        test(a3);

    }
    static void test(A a) {
        try { System.out.println(a); } 
        catch(Exception e) { System.out.println((String)null); }
    }
}

然后输出:

c:\files\j>javac A.java

c:\files\j>java A
null
2
null

接下来是,此时 a1 和 a3 符合 GC 条件,而 a2 则没有。

这个问题的教训是“将对象引用传递给方法并将该引用设置为 null 不会导致原始引用为 null”。这就是面试官试图测试的知识。

【讨论】:

  • 两个对象分别是a1和a3
  • 为了正确起见,我会将“两个对象被垃圾收集”更改为“两个对象有资格被垃圾收集”
  • @Justin 我笑了。 :) @matt true - 我的评论反映了这一点,但我的总结没有。固定。
  • 我认为A.go()A.easyMethod() 与否无关紧要。关于 GC,a3 引用的对象被调用孤立,因此有资格进行 GC。除非给出更多代码,否则b 是死代码。
  • @Jon 你不知道a2 是一个不必要的变量。它可以在// some other code 评论之后使用。无论如何,垃圾收集器可以看到a2 在主线程上,并且a2 指向的对象不符合垃圾收集条件。您无法保证编译器将或不会进行哪些优化。您只能相信它符合 JLS,并根据规范遵循您的代码中逻辑上会发生的情况。所以,是的,我错过了关于具有来自主线程的引用的对象如何无法访问并且有资格进行 GC 的观点。
【解决方案3】:

首先面试官对布尔值的理解是错误的——这段代码没有创建这样的对象,所以没有什么可以被垃圾收集。

ba2变量 称为垃圾回收是不正确的。对象是垃圾收集的,而不是变量。如果一个范围内的变量引用了一个对象,那么它就不能被垃圾回收。简单地说,只有当一个对象不再被任何变量引用时,它才能被垃圾回收。

所以我们在这段代码中创建了三个 A 实例。它们最初由a1 等引用,但由于变量引用发生了变化,我将对象实例称为A1、A2 和A3。由于您没有显示go 方法的定义,我将假设它是对easyMethod 的调用。

由于变量 a1 被重新赋值为 null,因此没有任何东西指向实例 A1,因此它可以被垃圾回收。

由于变量 a2 永远不会重新分配(easyMethod 中的分配不会影响原始变量),因此无法对实例 A2 进行垃圾回收。

由于easyMethod 总是返回null 并且a3 被分配了该方法的结果,所以没有任何东西指向实例A3,所以它也可以被垃圾回收。

【讨论】:

  • “如果一个范围内的变量引用了一个对象,那么它就不能被垃圾回收”。编译器中的活性分析会在其作用域结束之前从堆栈帧中删除死变量。如果它们不在堆栈上,则它们不是 GC 的全局根,因此它们所指的内容可以被垃圾收集。
【解决方案4】:

你能解释一下为什么 a2 并且 a3 没有被垃圾收集 ?

因为 a2 和 a3 不是对象。它们是变量。变量不可收集,原因很简单,它们不可分配。

【讨论】:

    【解决方案5】:

    对于 a2 的原始引用,它实际上完全取决于“其他代码”中发生的情况。如果“其他代码”不使用 a2 或 a3,则原始 a2 对象有资格进行垃圾回收。

    这是因为运行时不必关心 lexical 范围。它只需要知道一个对象永远不会被再次引用。因此,如果“一些其他代码”不使用 a2 或 a3,则它们指向的对象永远不会被再次引用,因此已经可用于垃圾回收。

    【讨论】:

    • 在 // 一些其他代码处。只有a2可以使用。 a1 和 a3 为空。
    • a1a3 最初引用的对象无法在“其他代码”处再次被引用,因此可以对它们进行 GC。 a2 所引用的对象则不能这样说。这完全不依赖于后面的代码。
    • @Justin:我从来没有暗示其他意思,但我理解您的困惑的根源,因此对其进行了编辑,希望能更好地反映我想说的话。
    • 谢谢,清楚多了!我将您的措辞“完全取决于”解释为意味着代码 sn-p 对任何对象是否会被 GC 没有影响。
    • 是的,它回到了必要与充分条件。在“其他代码”中不引用 a1/a2/a3 是获得 GC 资格的充分条件,但肯定不是必需的,因为实际上 a1/a3 的原始引用对象无论哪种方式都符合 GC 资格。 “完全取决于”显然不能正确传达这一点。
    【解决方案6】:

    你的问题很困惑。

    垃圾收集器处理对象,而不是变量。 因此,当您说 a2 有资格获得 GC 时,它没有任何意义。 你应该说在第 N 行被 a2 引用的对象在第 N+M 行符合 GC 条件。

    在您的示例中,您只有 3 个被实例化的对象。符合 GC 条件的是第一个 create A 和最后一个 create A 实例。

    【讨论】:

      【解决方案7】:

      问题是在// Some other code. 之前有多少对象可以进行垃圾回收

      这个问题很荒谬。

      垃圾收集器在运行时处理那里可用的信息。可达性由存储在寄存器、线程堆栈和全局变量中的全局根决定。寄存器和堆栈的内容是完全破坏代码的许多编译阶段的结果。源代码中的词法范围和行号的概念不再存在,因此询问 GC 可能在源代码中的某些点看到什么是无意义的,因为这些点在 GC 的世界中不存在。

      因此,我们必须首先假设源代码和生成的代码之间存在直接对应关系,否则我们甚至无法理解问题所指的代码点。我想不出有任何工作的虚拟机在实践中真正做到这一点,事实上,Java 可能具有甚至无法以这种方式编译为字节码的高级语言结构。

      接下来,我们必须假设一个模型用于将本地引用保存在堆栈中的方式。与其假设一些定义不明确的模型来得出随机答案,我将证明模型的选择允许我们得出从“没有任何东西符合 GC”到“一切都符合 GC 的条件”的答案。这一系列合理的答案确实突出了这个问题的糟糕程度。

      考虑一个简单的模型,其中中间值(所有子表达式以及变量的结果)被压入堆栈直到函数结束。简单的编译器和虚拟机(如 Windows Phone 7 上的 HLVM 和 .NET)在实践中实际上是这样工作的,尽管这会保留引用的时间超过必要的时间。使用此模型,每个new A() 都会将一个引用推入堆栈,直到函数返回,因此所有三个都可以从堆栈中的相关点访问,因此没有任何内容符合垃圾回收的条件。

      考虑一个模型,其中引用从堆栈中删除的第一个点不再读取它们。这更接近于生产虚拟机(如 .NET 和 OCaml)的工作方式,只是它们尽可能将本地引用保存在寄存器中,否则会溢出到函数调用堆栈帧中的预分配条目,覆盖死局部变量以最小化堆栈帧的大小。使用此模型,所有引用在相关点都已失效,因此无法访问,因此,所有三个新分配的对象以及 args 数组及其引用的所有字符串都符合垃圾回收条件。

      因此,我们不仅证明了从 nothingeverything 的答案是合理的,而且我们甚至引用了实现这些模型的真实工作虚拟机和垃圾收集器,因此我们的预测是可测试的。

      如果我在面试中被问到这个问题,我会通过解释其中的一些来测试面试官。如果他们通过表达对学习的兴趣反应良好,那么我仍然会对这份工作感兴趣。如果他们试图为自己定义不明确的假设和无法检验的预测辩护而反应不佳,那么我不想与他们合作。

      【讨论】:

      • “你标记的代码中的点是整个程序的结尾。” - 至少如果你处理注释// Some other code,就好像你是一个编译器,即跳过它。对我来说,评论的意思是表明那里还有一些代码,可能使用到目前为止定义的任何变量/字段,但确切发生的事情的细节与上下文无关这个问题的。所以恕我直言,保守的答案实际上是假设所有变量都在那里被进一步引用(必要时由空指针检查保护)。
      • @Jon 你在这里把举证责任放在了错误的牛身上。您是说“他们必须说明在评论下方使用了 a2 才能使其不符合条件”。我们是说“你必须假设它不符合条件,直到你能证明它没有在评论下方使用”。因为你无法证明它没有被使用,我们必须假设它被使用。
      • @Jon 你的假设实际上比我的要多。我的假设是,直到我们有证据证明没有进行任何优化,并且该元素直到方法结束才符合收集条件(此时我们所关注的尚未发生)。您假设一个元素将被标记为尽早进行垃圾回收作为优化(这很可能是正确的,但仍然是一个假设)。
      • @corsiKa “你假设......” 仅适用于我的一个示例。我的观点是,您可以从一系列假设中进行选择,并获得从无到有的结果。
      • “在几乎所有现实生活中,这都是学术上的吹毛求疵”。相反,其他答案中的隐含假设在实践和现实生活中完全没有用,正是因为它们没有做出任何可检验的预测。无法观察到效果。这显然与牛顿力学完全不同,牛顿力学可以并且已经过测试,并且在大多数情况下都非常准确。我对这个问题和大多数答案的抱怨是它具有极大的误导性。例如,您能否举出一个真正按照您假设的方式工作的虚拟机/GC?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-01-20
      相关资源
      最近更新 更多