gpt4 book ai didi

c# - GC.AddMemoryPressure() 不足以按时触发 Finalizer 队列执行

转载 作者:IT老高 更新时间:2023-10-28 12:51:56 27 4
gpt4 key购买 nike

我们为一个用 C# 编写的多媒体匹配项目编写了一个自定义索引引擎。

索引引擎是用非托管 C++ 编写的,可以以 std:: 集合和容器的形式保存大量非托管内存。

每个非托管索引实例都被一个托管对象包装;非托管索引的生命周期由托管包装器的生命周期控制。

我们已确保(通过自定义、跟踪 C++ 分配器)索引内部消耗的每个字节都被计算在内,并且我们更新(每秒 10 次)托管垃圾收集器的内存压力值,其增量为此值(正增量调用 GC.AddMemoryPressure(),负增量调用 GC.RemoveMemoryPressure())。

这些索引是线程安全的,可以被多个 C# worker 共享,因此可能有多个引用在使用同一个索引。出于这个原因,我们不能随意调用 Dispose(),而是依靠垃圾收集器来跟踪引用共享,并最终在索引未被工作进程使用时触发索引的终结.

现在,问题是内存不足。完整集合实际上执行得比较频繁,但是,在内存分析器的帮助下,我们可以在进程耗尽内存后耗尽内存的位置发现大量“死”索引实例被保存在终结队列中分页文件。

如果我们在内存不足的情况下添加一个调用 GC::WaitForPendingFinalizers() 后跟 GC::Collect() 的看门狗线程,我们实际上可以规避这个问题,然而,根据我们的阅读,手动调用 GC::Collect() 会严重破坏垃圾收集效率,我们不希望这样。

我们甚至添加了一个悲观的压力因子(尝试高达 4 倍)来夸大报告给 .net 端的非托管内存量,但无济于事,看看我们是否可以诱使垃圾收集器更快地清空队列.处理队列的线程似乎完全没有意识到内存压力。

此时我们觉得我们需要在计数达到零后立即对 Dispose() 实现手动引用计数,但这似乎有点矫枉过正,尤其是因为内存压力API正是为了解决像我们这样的情况。

一些事实:

  • .Net 版本为 4.5
  • 应用处于 64 位模式
  • 垃圾收集器正在并发服务器模式下运行。
  • 索引的大小约为 800MB 的非托管内存
  • 在任何时间点最多可以有 12 个“事件”索引。
  • 服务器有 64GB 内存

欢迎提出任何想法或建议

最佳答案

嗯,没有答案,但“如果你想明确地处理外部资源,你必须自己做”。

AddMemoryPressure() 方法不保证立即触发垃圾回收。相反,CLR 使用非托管内存分配/释放统计信息来调整它自己的 gc 阈值,并且只有在认为合适时才会触发 GC。

请注意,RemoveMemoryPressure() 根本不会触发 GC(理论上它可以这样做,因为设置 GCX_PREEMP 等操作的副作用,但为了简洁起见,我们跳过它)。相反,它会降低当前的 mempressure 值,仅此而已(再次简化)。

实际算法未记录,但您可以查看实现 from CoreCLR .简而言之,您的 bytesAllocated 值必须超过某个动态计算的限制,然后 CLR 才会触发 GC。

现在是坏消息:

  • 在实际应用中,这个过程是完全不可预测的,因为每次 GC 收集和每个第三方代码都会对 GC 限制产生影响。 GC可能会被调用,可能会被稍后调用,可能根本不会被调用

  • GC 调整它限制尝试最小化昂贵的 GC2 集合(您对这些感兴趣,因为您正在使用长生命周期的索引对象,并且由于终结器,它们总是被提升到下一代)。因此,使用巨大的内存压力值对运行时进行 DDOS 可能会反击,因为您会将标准提高到足够高,以使(几乎)没有机会通过设置内存压力来触发 GC。(注意:最后一个问题将用 new AddMemoryPressure() implementation 修复,但今天肯定不会)。

UPD:更多详情。

好的,让我们继续:)

第 2 部分,或“较新的低估 _udocumented_ 的含义”

正如我上面所说,您对 GC 2 集合感兴趣,因为您使用的是长生命周期对象。

众所周知,终结器几乎在对象被 GC 后立即运行(假设终结器队列没有被其他对象填充)。作为证明:只需运行 this gist .

您的索引没有被释放的真正原因非常明显:对象所属的世代没有被 GC。现在我们回到最初的问题。您如何看待,您必须分配多少内存才能触发 GC2 收集?

正如我上面所说,实际数字没有记录。理论上,GC2 可能根本不会被调用,直到你消耗了非常大的内存块。而现在真正的坏消息来了:对于服务器 GC,“理论上”和“实际发生的情况”是相同的。

One more gist ,在 .Net4.6 x64 上,输出将是这样的:

GC low latency:
Allocated, MB: 512.19 GC gen 0|1|2, MB: 194.19 | 317.81 | 0.00 GC count 0-1-2: 1-0-0
Allocated, MB: 1,024.38 GC gen 0|1|2, MB: 421.19 | 399.56 | 203.25 GC count 0-1-2: 2-1-0
Allocated, MB: 1,536.56 GC gen 0|1|2, MB: 446.44 | 901.44 | 188.13 GC count 0-1-2: 3-1-0
Allocated, MB: 2,048.75 GC gen 0|1|2, MB: 258.56 | 1,569.75 | 219.69 GC count 0-1-2: 4-1-0
Allocated, MB: 2,560.94 GC gen 0|1|2, MB: 623.00 | 1,657.56 | 279.44 GC count 0-1-2: 4-1-0
Allocated, MB: 3,073.13 GC gen 0|1|2, MB: 563.63 | 2,273.50 | 234.88 GC count 0-1-2: 5-1-0
Allocated, MB: 3,585.31 GC gen 0|1|2, MB: 309.19 | 723.75 | 2,551.06 GC count 0-1-2: 6-2-1
Allocated, MB: 4,097.50 GC gen 0|1|2, MB: 686.69 | 728.00 | 2,681.31 GC count 0-1-2: 6-2-1
Allocated, MB: 4,609.69 GC gen 0|1|2, MB: 593.63 | 1,465.44 | 2,548.94 GC count 0-1-2: 7-2-1
Allocated, MB: 5,121.88 GC gen 0|1|2, MB: 293.19 | 2,229.38 | 2,597.44 GC count 0-1-2: 8-2-1

没错,在最坏的情况下,您必须分配 ~3.5 gig 才能触发 GC2 收集。我很确定您的分配要小得多:)

注意:请注意,处理 GC1 生成的对象并不会让它变得更好。 GC0 段的大小可能超过 500mb。您必须非常努力地触发 ServerGC 上的垃圾收集 :)

总结:使用 Add/RemoveMemoryPressure 的方法(几乎)对垃圾收集频率没有影响,至少对服务器 GC 而言。

现在,问题的最后一部分:我们有哪些可能的解决方案?简而言之,最简单的方法是通过一次性包装器进行引用计数。

待续

关于c# - GC.AddMemoryPressure() 不足以按时触发 Finalizer 队列执行,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33508695/

27 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com