【问题标题】:collection Clear() vs new, GC impact集合 Clear() 与新的 GC 影响
【发布时间】:2025-12-05 09:25:01
【问题描述】:

我在一个循环中创建了一堆 MemoryStream 并将它们添加到一个集合(在本例中为 ArrayList)。
之后我遍历这个列表并处理这个流。 因为我遇到了 Outofmemoryexceptions,我决定改为定期处理列表并释放它。

通过list = new ArrayList() 执行此操作并没有改变内存消耗,无论是在监控它时,还是在消除内存不足异常时。甚至调用 GC.Collect() 也没有改变这一点。我注意到只有在离开范围后才释放内存。

调用List.Clear()然而,立即释放了内存并且循环按预期工作。

那么,为什么会有这种差异?这里的许多其他主题给人的印象是这两种方法应该基本相同,list = new ArrayList() 可能更有效,因为 Clear() 是一个 O(n) 操作。

我很确定没有其他对我的内存流的引用(我基本上是 list.Add(new MemoryStream(...)

【问题讨论】:

  • 不是答案,而是与细节相关:*.com/questions/5358129/…
  • 是否保证旧列表有 no 额外的强可达参考?代码是为 Debug 还是 Release 构建的?调试模式可能会产生比预期更长的“挂起”引用的效果,因为可能会意外地保持对旧列表的引用处于活动状态。如果是这种情况,通过 List.Clear 间接“清除”引用可能不会产生相同的效果。
  • 在家里,我现在无法访问真正的代码,但我很确定我没有忽略另一个参考。但调试模式可能是罪魁祸首。无论如何,我希望 GC 在发生异常之前尽可能地完成它的工作......
  • 和我一样,这就是为什么我怀疑旧的列表(或列表)以某种方式在某个地方是强可达的——这反过来又会使包含的项目保持强可达,从而阻止 GC 回收他们。如果是这种情况,那么简单地清除列表将“解决”问题,因为列表本身不再强烈引用先前的元素,即使它本身保持强可达。 (或者这可能是一个奇怪的怪事耸耸肩。)

标签: .net garbage-collection


【解决方案1】:

嗯,不同的。 ArrayList.Clear() 将所有元素设置为空。这使得这些元素立即有资格收集。

如果您重新分配 ArrayList,那么收集原始 ArrayList 的时间很重要。只有 then 元素也会被收集。如果原始 ArrayList 很大(超过 7083 个项目),那么它的底层数组将最终出现在大对象堆中。不经常收集。所以元素也会停留一段时间。增加OOM的几率。

您应该在这里看大局,您的程序在仍然能够完成工作的边缘摇摇欲坠。随着时间的推移,这种情况很少会变得更好。您需要认真考虑进行彻底的重写,例如,将 VM 使用量减半,这样您就可以在一段时间内有一些喘息的空间。或者采取极其简单的解决方案。拨动开关并瞄准 64 位操作系统。今天广泛使用。

【讨论】:

  • 你是对的,当然,我已经重写了该代码以更加资源友好。我只是想知道不同的行为。如果您看一下我自己的答案,那么当 new 位于循环的末尾或开头时甚至会有所不同...
【解决方案2】:

在调试模式下,JIT 将所有局部变量的生命周期(物理上:堆栈位置和保存引用的寄存器)延长到函数的末尾。您可以看到随机对象的生命周期延长。

此行为不违反 GC 保证。从不删除任何内容的 GC 是有效的 GC,尽管它没有用处。

在这里将变量显式清除为 null 并分解出函数会有所帮助。

当您使用list = new ArrayList() 覆盖引用变量时,可能仍有其他对象引用到旧列表。它们可能在代码中的某处是显式的,或者只是碰巧仍保留旧引用但未使用的随机局部变量。

闭包也容易捕获引用。

【讨论】:

    【解决方案3】:

    根据@user2864740 的评论,我写了一个小测试例程,他们是对的:效果只出现在调试模式下。此外,仅当 i new 列表位于循环的 end 时,而不是将同一语句移至开头:

        static void Main(string[] args)
        {
            using (StreamWriter w = new StreamWriter(@"d:\tststream.123", false, Encoding.Default))
                for (int i = 0; i < (1 << 20); i++) 
                    w.WriteLine(Guid.NewGuid());
    
            List<MemoryStream> list = new List<MemoryStream>();
            for (int j = 0; j < 100; j++)
            {
                for (int i = 0; i < 30; i++)
                {
                    list.Add(new MemoryStream(File.ReadAllBytes(@"d:\tststream.123")));
                }
                list = new List<MemoryStream>();
                Console.WriteLine(j.ToString());
            }
        }
    

    在调试模式下编译时,在第二次迭代时会抛出内存不足(当然是 32 位)。在发行版中编译它或将list = new List&lt;MemoryStream&gt;(); 移动到循环的开头,它会“无限期地”继续。

    【讨论】: