【问题标题】:Java: Reusing vs Reallocating reference to container object?Java:重用与重新分配对容器对象的引用?
【发布时间】:2015-09-21 10:13:52
【问题描述】:

tl;dr:在Java中,最好是重用容器对象或每次都创建对象,让垃圾收集器完成工作

我在 Java 中处理大量数据,其中经常有以下类型的代码结构:-

版本 1:

for(...){//outer loop
   HashSet<Integer> test = new HashSet<>(); //Some container
   for(...){
      //Inner loop working on the above container Data Structure
   }
   //More operation on the container defined above
}//Outer loop ends

这里我每次循环分配新内存,并在内部/外部循环中进行一些操作,然后再次分配空内存。

现在我担心 Java 中的内存泄漏。我知道 Java 有一个相当不错的垃圾收集器,但我应该修改我的代码,而不是依赖它:-

版本 2:

HashSet<Integer> test = null;
for(...){//outer loop
   if(test == null){
      test = new HashSet<>(); //Some container
   }else{
      test.clear()
   }
   for(...){
      //Inner loop working on the above container Data Structure
   }
   //More operation on the container defined above
}//Outer loop ends

我有三个问题:-

  1. 哪个性能更好,或者没有明确的答案。
  2. 第二个版本的时间复杂度会更高吗?换句话说,在复杂度上是 O(n) 的 clear() 函数 O(1)。我在 javadocs 中什么都没有。
  3. 这种模式比较常见,哪个版本比较推荐?

【问题讨论】:

  • clear 可能会稍微快一些(上次我检查了 10/15%)。最好的方法是用您的数据测试这两种方法。在正常程序中,这不会成为瓶颈。
  • @assylias 我认为的一个原因是每次都必须调整内存大小如果我每次都分配新内存。但是空间复杂度呢?如果速度不是问题,是否会对内存占用产生重大影响?
  • @TagirValeev 请原谅我,我在那里很心不在焉。我打算进行空检查,如果容器从未被初始化,我会提供内存,否则我会清理并重用我拥有的空间

标签: java performance garbage-collection time-complexity hashset


【解决方案1】:

在我看来,最好使用第一种方法。请注意,HashSet.clear 永远不会缩小哈希表的大小。因此,如果外循环的第一次迭代将许多元素添加到集合中,哈希表将变得非常大,但在随后的迭代中,即使需要更少的空间,也不会缩小。

第一个版本还使进一步的重构变得更容易:您以后可能希望将整个内部循环放入单独的方法中。使用第一个版本,您可以将其与HashSet 一起移动。

最后请注意,对于垃圾回收而言,管理短期对象通常更容易。如果你的HashSet 是长期存在的,它可能会被移到老年代并且仅在完整的GC期间被移除。

【讨论】:

  • 谢谢,重构很有趣。但是关于大小,清除后,下一个元素将在已经扩展的空间中被覆盖。这将节省时间,因为在容器中重新调整大小非常昂贵,并且最大大小将是最大(所有迭代)
  • @MangatRaiModi,如果你可以提前估计你需要多少元素,只需将参数传递给HashSet构造函数,以避免重新散列。
  • 我想过,但它真的很难,因为元素被添加到 Hashset 受到各种条件的影响,根据估计过度提交是不是一种不好的做法?
  • @MangatRaiModi,与往常一样,这是一种权衡。如果您高估,您将占用比必要更多的内存,但不需要重新散列。如果您低估了,您将需要重新哈希,但消耗更少的内存。
【解决方案2】:

我认为每次创建一个新的 HashSet 更简单,并且以后可能不太容易出现重构错误。除非您有充分的理由重新使用 HashSet(垃圾收集暂停对您来说是一个问题,并且分析显示这部分代码是原因) - 我会尽可能简单并坚持 1. 专注于可维护性,应该避免Premature Optimization

【讨论】:

    【解决方案3】:

    我建议您坚持第一个变体。这背后的主要原因是保持 HashSet 变量的范围尽可能小。这样,您实际上可以确保在迭代结束后它有资格进行垃圾收集。提升它的作用域可能会导致其他问题 - 引用可以在以后用于实际更改对象的状态。

    此外,如果您在循环内部或外部创建实例,大多数现代 Java 编译器都会生成相同的字节码。

    【讨论】:

    • 如果您在循环内部或外部创建实例,大多数现代 Java 编译器都会生成相同的字节码。。为这条线 +1 :)
    【解决方案4】:

    哪个更快?实际上,答案可能会因各种因素而异。

    版本 1 的优势:

    1. 处理器级别的预测分支可能会加快速度。
    2. 实例范围仅限于第一个循环。如果引用没有转义,JIT 可能实际上会编译您的方法。 GC的工作将 可能会更容易。

    版本-2:

    1. 减少创建新容器的时间(坦率地说,这并不算多)。
    2. clear()O(n)
    3. 转义引用可能会阻止 JIT 进行一些优化。

    选择哪一个? 多次测量两个版本的性能。然后,如果您发现显着差异,请更改您的代码,如果没有,请不要做任何事情:)

    【讨论】:

    • 明确是 O(n) 是非常糟糕的,我认为 Java 会有一些标志,例如:正在使用/转储。我想我最好坚持使用version1。感谢您的帮助。
    • @MangatRaiModi - 是的,不幸的是,对于 clear(),您必须将 容器 的每个单元格设置为 null(如果您有泄漏的引用怎么办? )。所以它是O(n)。我感觉版本 1 会更快:)
    • 哎呀,没有考虑泄漏的引用,我只考虑原始数据类型。所以,它必须是 O(n)。
    【解决方案5】:

    版本 2 更好 但这会花费更多时间,但内存性能会很好

    【讨论】:

    • 一些解释真的很有帮助!
    • 我不认为版本 2 会更快 madamji :)。事实上,假设一个版本比另一个版本更快是不正确的。根据 JVM (JIT) 和底层系统架构,一个版本在一个系统上可能更快,但在另一个系统上更慢:)
    • @TheLostMind 沮丧的印度人发现:p [Off Topic]
    • @MangatRaiModi - 沮丧?究竟是谁,为什么?
    • @TheLostMind ,Sirji 我在那里写过它不会更快,但内存性能会有效
    【解决方案6】:

    视情况而定。

    回收对象可以在紧密循环中用于消除 GC 压力。尤其是当对象对于年轻代来说太大或者循环运行时间足够长以使其成为永久对象时。

    但在您的特定示例中,它可能没有多大帮助,因为哈希集仍然包含节点对象,这些节点对象将在插入时创建并在清除时符合 GC 条件。

    另一方面,如果您将太多项目放入集合中,以至于其内部 Object[] 数组必须多次调整大小并且对于年轻一代来说变得太大,那么回收集合可能很有用。但在这种情况下,无论如何您都应该预先调整集合的大小。

    此外,仅在代码块期间存在的对象可能有资格通过escape analysis 进行对象分解/堆栈分配。它们的生命周期越短,涉及这些对象的代码路径越不复杂,EA 成功的可能性就越大。

    最后,这并不重要,直到此方法真正成为您应用程序中的分配热点,在这种情况下,它会显示在分析器结果中,您可以采取相应的行动。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-07-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-01-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多