- mongodb - 在 MongoDB mapreduce 中,如何展平值对象?
- javascript - 对象传播与 Object.assign
- html - 输入类型 ="submit"Vs 按钮标签它们可以互换吗?
- sql - 使用 MongoDB 而不是 MS SQL Server 的优缺点
我们为一个用 C#
编写的多媒体匹配项目编写了一个自定义索引引擎。
索引引擎是用非托管 C++
编写的,可以以 std::
集合和容器的形式保存大量非托管内存。
每个非托管索引实例都被一个托管对象包装;非托管索引的生命周期由托管包装器的生命周期控制。
我们已确保(通过自定义、跟踪 C++ 分配器)索引内部消耗的每个字节都被计算在内,并且我们更新(每秒 10 次)托管垃圾收集器的内存压力值,其增量为此值(正增量调用 GC.AddMemoryPressure()
,负增量调用 GC.RemoveMemoryPressure()
)。
这些索引是线程安全的,可以被多个 C# worker 共享,因此可能有多个引用在使用同一个索引。出于这个原因,我们不能随意调用 Dispose()
,而是依靠垃圾收集器来跟踪引用共享,并最终在索引未被工作进程使用时触发索引的终结.
现在,问题是内存不足。完整集合实际上执行得比较频繁,但是,在内存分析器的帮助下,我们可以在进程耗尽内存后耗尽内存的位置发现大量“死”索引实例被保存在终结队列中分页文件。
如果我们在内存不足的情况下添加一个调用 GC::WaitForPendingFinalizers()
后跟 GC::Collect()
的看门狗线程,我们实际上可以规避这个问题,然而,根据我们的阅读,手动调用 GC::Collect()
会严重破坏垃圾收集效率,我们不希望这样。
我们甚至添加了一个悲观的压力因子(尝试高达 4 倍)来夸大报告给 .net 端的非托管内存量,但无济于事,看看我们是否可以诱使垃圾收集器更快地清空队列.处理队列的线程似乎完全没有意识到内存压力。
此时我们觉得我们需要在计数达到零后立即对 Dispose()
实现手动引用计数,但这似乎有点矫枉过正,尤其是因为内存压力API正是为了解决像我们这样的情况。
一些事实:
欢迎提出任何想法或建议
最佳答案
嗯,没有答案,但“如果你想明确地处理外部资源,你必须自己做”。
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/
我正在创建一个使用 svg 作为::after 内容的标签。我希望通过使用绝对定位将 svg 移动到主要内容下方并将其设置为 bottom: 0,但是如果我也将 margin-bottom 设置为 -
我是一名优秀的程序员,十分优秀!