不久前,一篇关于高负载.NET服务中的垃圾回收的 HabréOptimization上的精彩文章出现了。 本文非常有趣,因为具有理论的作者较早地完成了不可能的事情:他们使用GC的知识优化了应用程序。 而且,如果以前我们不知道该GC如何工作,现在通过Konrad Cocos在他的著作Pro .NET Memory Management中的努力,可以将其呈现给我们。 我为自己得出了什么结论? 让我们列出问题区域并考虑如何解决它们。
在最近的CLRium#5研讨会:垃圾收集器上,我们整天都在谈论GC。 但是,我决定发布一份带有文本解码的报告。 这是关于有关应用程序优化的结论的讨论。
减少跨代连接
问题
为了优化垃圾收集速度,GC会尽可能收集年轻的一代。 但是要做到这一点,他还需要有关卡片表中来自较早几代的链接的信息(在这种情况下,它们充当额外的根)。
同时,从长辈到年轻一代的一个链接会迫使您用纸牌桌覆盖该区域:
- 4个字节重叠4 kb或最大 320个对象-用于x86体系结构
- 8个字节重叠8 kb或最大 320个对象-用于x64体系结构
即 GC检查卡表中的值达到非零值,因此我们被迫检查最多320个对象,以确定我们这一代是否存在传出链接。
因此,年轻一代中的稀疏链接将使GC更加耗时
解决方案
- 在附近的年轻一代中找到具有联系的对象;
- 如果假定为零生成对象,请使用拉动。 即 创建一个对象池(不会有新对象:不会有零代对象)。 此外,通过使用两个连续的GC“预热”池,以确保其内容在第二代中失败,从而避免了链接到较年轻的一代并在卡表中设置零。
- 避免与年轻一代建立联系;
避免强连接
问题
根据SOH中对象压缩阶段的算法,如下所示:
- 要压缩堆,您需要遍历树并检查所有链接,并为新值更正它们。
- 此外,卡片表中的链接会影响整个对象组
因此,对象的一般强连通性可能导致GC期间下沉。
解决方案
- 一代人地将附近的物体牢固连接
- 通常避免不必要的链接(例如,不要复制this->句柄链接,而使用已经存在的this-> Service->句柄)
- 避免使用隐藏连接的代码。 例如,关闭
监控细分受众群的使用情况
问题
在密集工作中,可能会出现以下情况:分配新对象导致延迟:在堆下分配新段,并在清理垃圾时进一步取消使用这些段
解决方案
- 使用PerfMon / Sysinternal实用程序控制新段的选择点以及它们的解除和释放
- 如果我们谈论的是LOH(这是密集的缓冲区流量),请使用ArrayPool
- 如果要谈论SOH,请确保在附近突出显示相同寿命的对象,并提供Sweep而不是Collect
- SOH:使用对象池
不要在已加载的代码段中分配内存
问题
代码的已加载部分分配内存:
- 结果,GC选择的分配窗口不是1Kb,而是8Kb。
- 如果窗口空间不足,则会导致GC并扩大封闭区域
- 密集的新对象流将使其他线程中的短寿命对象迅速进入垃圾回收条件更差的较早一代
- 这将增加垃圾收集时间
- 即使在并发模式下,也将导致更长的Stop the World停止世界
解决方案
- 完全禁止在代码的关键部分使用闭包
- 完全禁止对代码的关键部分进行装箱(如有必要,可以通过拉动使用仿真)
- 在需要创建用于存储数据的临时对象的地方,请使用结构。 引用结构更好。 当字段数大于2时,由ref发送
避免在LOH中分配不必要的内存
问题
将阵列放置在LOH中会导致GC程序碎片化或加重
解决方案
- 使用将数组划分为子数组和封装处理此类数组的逻辑的类(即,代替存储大型数组的List <T>,将其MyList与array [] []分开,将数组划分得更短)
- 阵列将进入SOH
- 经过几次垃圾收集后,它们将躺在永生的物体旁边,并不再影响垃圾收集
- 控制长度超过1000个元素的双精度数组的使用。
在可能的情况下,使用线程堆栈
问题
方法调用(包括内部调用)中存在许多超短对象或对象。 他们创建对象流量
解决方案
- 尽可能在堆栈上使用内存分配:
- 使用
Span T x = stackalloc T[];
尽可能替换new T[]
- 尽可能使用
Span/Memory
- 将算法转换为
ref stack
类型(StackList:struct, ValueStringBuilder )
尽早释放对象
问题
被认为是短暂的,对象属于gen1,有时属于gen2。
这导致较重的GC使用寿命更长
解决方案
- 您必须尽早释放对象引用
- 如果冗长的算法包含可用于任何对象的代码,则该代码将按代码隔开。 但是可以将其分组到一个位置,因此有必要将其分组,从而可以更早地收集它们。
- 例如,在第10行上,将集合取出,在第120行上,将其过滤掉。
无需调用GC.Collect()
问题
通常,如果您调用GC.Collect(),它将解决此问题。
解决方案
- 学习GC操作算法,查看ETW下的应用程序和其他诊断工具(JetBrains dotMemory,...)更为正确。
- 优化问题最严重的区域
避免固定
问题
固定会带来许多问题:
- 使垃圾收集复杂化
- 创建可用内存空间(节点空闲列表项,砖块表,存储桶)
- 在卡片表中形成链接时,可能会在年轻一代中留下一些物体
解决方案
如果没有其他出路,请使用fixed(){}。 这种提交方法并不构成真正的提交:只有当GC在花括号内工作时才会发生。
避免定案
问题
确定性调用不是确定性的:
- 未经邀请的Dispose()会最终确定对象的所有传出链接
- 相关对象的延迟时间比计划的时间长
- 老龄化,移居到老一辈
- 如果它们同时包含到较年轻的链接,则它们将从卡表生成链接
- 使老一辈的组装复杂化,将它们分割成碎片,从而导致压缩而不是扫描
解决方案
轻轻调用Dispose()
避免线程过多
问题
随着线程数量的增加,分配上下文随着 它们分配给每个线程:
- 结果,GC.Collect变得更快。
- 由于临时段空间不足,Collect将跟随Collective Sweep
解决方案
避免流量不同大小的物体
问题
当流量不同大小和生命周期的对象时,会发生碎片:
- 增加碎片率
- 在所有引用对象中以地址更改阶段触发集合触发
解决方案
如果假定对象流量:
- 检查是否存在额外的字段,近似大小
- 检查是否缺少字符串操作:如果可能,请替换为ReadOnlySpan / ReadOnlyMemory
- 尽快释放链接
- 利用拉力
- 使用双GC温暖的缓存和池以压缩对象。 因此,可以避免卡牌桌出现问题。