【问题标题】:Verify object deletion in Java在 Java 中验证对象删除
【发布时间】:2017-03-17 12:24:32
【问题描述】:

在我的代码中,在运行时创建和删除了许多对象,这些对象可能有很长的引用链(即可以从许多地方进行垃圾收集)。

在我的单元测试中,我想验证当我“删除”一个对象时,例如从主要包含它的列表中删除它,对该对象的所有引用都将被删除。明确一点:我想验证我的应用程序中不再存在对此对象的引用。

我怎样才能做到这一点?

到目前为止,我只想到了这个(测试 PhantomReference 是否已入队):

@Test
public void test_objectX_can_be_garbage_collected() throws Exception {

    ReferenceQueue<MyClass> refsToBeDeleted = new ReferenceQueue<>();
    MyClass obj = new MyClass();
    PhantomReference<MyClass> phantomRef = new PhantomReference<MyClass>(obj, refsToBeDeleted);

    obj = null;   // delete last strong reference to obj
    System.out.println("Reference is enqueued:" + phantomRef.isEnqueued()); // "false"

    System.gc();   // invoke garbage collection
    Thread.sleep(1000L);   // required - see text
    System.gc();   // invoke garbage collection again
    System.out.println("Reference is enqueued:" + phantomRef.isEnqueued()); // true
}

public static class MyClass {
}

doc 说:

公共静态 void gc()

运行垃圾收集器。 调用 gc 方法表明 Java 虚拟机花费精力回收未使用的对象,以使它们当前占用的内存可用于快速重用。 当控制从方法调用返回时,Java 虚拟机已尽最大努力从所有丢弃的对象中回收空间。

(强调我的)

这是相当模糊的(“尽力而为”),并且似乎非常不确定,例如我必须等待 1 秒并再次致电 gc(),或者将其称为例如连续四次将引用排入队列。

有没有办法正确地做到这一点?我想测试对象不会持续存在,并且在几个月的正常运行后最终会淹没我的记忆。

备注:

1.) 我知道你不应该打电话给gc(),因为它是糟糕的代码。没错,但我是从测试代码调用它来验证生产代码。

2.) 我知道gc() 从本地的角度来看实际上不是确定性的。我知道这一点并寻求更好的选择。

编辑:

我意识到这个可以是一种“特殊”的单元测试:如果它对给定的代码库成功一次,我知道这个引用是垃圾收集的;如果单元测试失败,可能是因为gc() 不想删除它,或者它无法删除。因此,此测试可以验证内存是否已释放,但可能需要多次尝试才能进行验证。然而,这是“特殊的”(读作:“坏的”),因为我必须接受随机失败的单元测试。

【问题讨论】:

  • 使用 Java 内存分析器会更容易吗?
  • @SteveSmith 可能是这种情况 - 我不得不承认我从未使用过...但是我想自动测试它。
  • 一个随机的危险和hacky想法:启动一个具有相对较小堆的新VM。在此 VM 中创建对象,然后删除所需的对象,然后创建一个无限链表来消耗堆,直到抛出 OutOfMemoryError,从而确保执行 gc。检查引用现在是否已清除。如果清除,则测试通过。如果不是,则测试失败。这将是确定性的,但可能非常难以创建和维护。我不确定 VM 在 OutOfMemoryError 之后的行为如何,因此这个想法可能根本行不通。是否为我的愚蠢建议付出努力和时间由您决定。
  • 恕我直言,您可以在测试中依赖System.gc。它不是随机的,它始终有效,除非关闭。只要没有人这样做,它就不会关闭。所以不要这样做并保护你的计算。 :D
  • @maaartinus 谢谢,但我不明白为什么我必须调用 System.gc() 几次 (或者在睡眠之间,或者三到四次,在我的机器上)成功地将该引用排入队列。我怎么知道“正确”的频率是多少?这不是在文档中指定的方式(“当 gc 返回时,它已经尽最大努力......”),因为如果在连续三个调用后尽力而为成功,那么第一次调用不是尽力而为恕我直言。

标签: java unit-testing testing garbage-collection


【解决方案1】:

AFAIK 无法确保对象已被垃圾回收。调用System.gc() 通常会导致垃圾回收,但并非总是如此。

我还有一个无限运行的 Java 进程 - 我在每次迭代开始时调用 System.gc() 并记录所使用的内存量。如果这个数量随着时间的推移没有显着变化,我可以合理地确定不会“泄漏”内存。

【讨论】:

    【解决方案2】:

    我观察到,当触发堆转储时,它通常会执行完整的 GC。如果这是 JVM 在堆收集期间强制 GC 的策略,那么您可以通过

    触发堆收集
    kill -3 pid
    

    看看它是否有效。

    您可能需要添加 jvm 参数以获取堆转储。

    【讨论】:

      【解决方案3】:

      我没有看到使用引用和System.gc() 进行测试的问题(尽管我更喜欢弱引用而不是虚引用)。我不记得这种方法有任何稳定性问题。

      仅当您可以获取对可能泄漏的对象的引用时,基于引用的方法才有效。

      我有来自 NetBeans 的 forked version 头转储分析库,我用它来自动化某些类型的内存使用报告。

      对于测试代码,可以获取自身的堆转储并检查此转储断言存在的给定类型的特定数量的对象。

      Here 你可以找到这种方法的例子。

      【讨论】:

      • 听起来很有趣。我从未使用过 NetBeans 堆分析器;您知道“堆上”是否仍然存在无法访问的对象(即在未被垃圾回收时由heap.getAllInstances()(在示例中)返回)?
      • 视情况而定。堆转储命令可以按原样转储堆,也可以完全 GC 和转储。在测试中,我使用第二个选项。虽然 API 也允许转储未收集的堆。
      • System.gc() 不能保证做任何事情。没有什么能阻止 JVM 供应商将其实现为 nop。此外,您不能每次都依赖它进行垃圾收集(甚至不知道您将获得的是完整还是部分 GC),因此永远不要在确定性测试中使用 System.gc。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-07-28
      • 1970-01-01
      相关资源
      最近更新 更多