哪家建站公司好,wordpress 宝塔加速,大型大型网站建设,柳州建设厅官方网站本文90%通过机器翻译#xff0c;另外10%译者按照自己的理解进行翻译#xff0c;和原文相比有所删减#xff0c;可能与原文并不是一一对应#xff0c;但是意思基本一致。译者水平有限#xff0c;如果错漏欢迎批评指正译者Bing Translator、InCerry#xff0c;另外感谢Hex、… 本文90%通过机器翻译另外10%译者按照自己的理解进行翻译和原文相比有所删减可能与原文并不是一一对应但是意思基本一致。译者水平有限如果错漏欢迎批评指正译者Bing Translator、InCerry另外感谢Hex、晓青、贾佬、黑洞百忙之中抽出时间帮忙review和检查错误。原文链接https://devblogs.microsoft.com/dotnet/put-a-dpad-on-that-gc/这是在说什么是的我们有一个在区域【原文叫region】上叫做DPAD的新功能。区域是我们目前在.NET 6中用于替换段【原文叫segment】的新东西。在这篇博文中我将首先对区域做一些介绍然后谈谈DPAD功能。请注意我们不太可能在.NET 6.0结束时正式支持区域因为这涉及到很多工作--我们目前的计划是在clrgc.dll中把它作为一个实验性的功能你可以通过配置来打开。事实上这就是我希望从现在开始的大型GC功能的发布方式我们首先将它们与独立的GC一起发布即在clrgc.dll中这样人们就可以尝试它们然后我们在coreclr.dll中正式开启它们这样它们就默认开启了。译者注原本.NET的GC是分段式GC也就是说GC管理内存的单位是段而现在改了改成区域了另外这一段中Maoni大佬其实透露三个重要的信息段内存分配的方式结束了将使用区域的方式来替代段内存分配。.NET 6.0中大概率不会支持区域但是会通过clrgc.dll的方式独立提供你可以通过配置的方法打开大家要注意这个独立提供因为从.NET Core 2.1开始我们就可以自定义GC了也就是说你开心的话可以自己写一个GC然后替换掉.NET自带的GC使用的环境变量是这个link另外也有大佬实现了一个Zero GC link你只需要实现几个接口就可以自定义GC。以后.NET上GC重大功能的发布都会遵循这样一个步骤功能开发 单独发布到clrgc.dll 公开测试修复bug 正式发布到coreclr.dll到目前为止如你所知我们一直在段上运作。段多年来为我们提供了很好的服务但我开始注意到它的局限性因为人们把更多种类的工作负载放在我们的框架上。段是我们内存管理的基础所以从段转换成区域是件大事。当我们接近.NET 6发布时我决定是时候摆脱段式了所以这是我们的团队最近花费大量时间的地方。那么段和区域之间的主要区别是什么段是大的内存单位--在Server GC 64-bit上如果段的大小是1GB、2GB或4GB在工作站模式下更小-256MB而区域是小得多的单位它们默认为每个4MB。所以你可能会问所以它们更小为什么有意义。要回答这个问题首先让我们回顾一下段是如何工作的。如果您看不明白上面的这一段文字那么建议您先补一下基础的知识微软的官方文档。里面详细的介绍了.NET GC的基础知识包括什么是分代、垃圾回收的过程、服务器GC与工作站GC、并发GC、后台GC等等。目前当我们只有一个段时SOH在堆上是这样的当我们有多个段时它可以看起来像这样或这样蓝色和黄色的空间是一个段上所有已提交【已提交是指由操作系统分配给应用程序使用的内存】的内存关于Gen【代】开始的解释请看这个视频。每个段都会记录该段上已提交的内容以便我们知道是否需要提交更多。而该段上的所有空闲空间也是已提交的内存。当我们使用空闲空间来容纳对象时这很有效因为我们可以立即使用内存--它已经被提交。但是想象一下这样的场景我们在某一代有空闲空间比如说gen0因为有一些异步IO正在进行导致我们在gen0中降级了一堆pin对象但我们实际上并没有使用这可能是由于没有等待这么长时间来做下一次GC或者我们已经积累了太多的活着的对象这意味着GC暂停会太长。如果我们能将这些空闲空间用于其他代如果他们需要的话那不是很好吗gen2和LOH中的空闲空间也是一样的--你可能在gen2中有一些空闲空间如果能用它们来分配一些大的对象就好了。我们在段上做撤销提交【uncommit已提交的反向操作】但只是在段的末端也就是在该段上最后一个活对象之后由每个段末端的浅灰色空间表示。而如果你有pin对象就阻止了GC收回段的末端那么只能形成自由空间而自由空间里是已提交的内存。当然你可能会问为什么不直接把有大量自由空间的段的中间部分取消提交。但这需要记录以记住段中间的哪些部分被解密所以当我们想用它们来分配对象时我们需要重新提交它们。而现在我们已经进入了区域的概念也就是让更小的内存量被GC单独操作。如果您看不懂上面这段文字那么说明您需要翻阅一下下面这些资料来了解已提交内存、pin对象、固定对象堆等等https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/fixed-statementhttps://zhuanlan.zhihu.com/p/376419012有了区域各代人看起来是统一的我们不再有这种 短暂的片段 概念。我们有gen0和gen1区域就像我们有gen2区域一样。当然每一代的区域数量可能有很大的不同。但它们都由这些小的内存单元组成。LOH的区域确实更大LOH是SOH区域大小的8倍所以每个32MB。当我们释放一个区域时我们将其返回到自由区域池中该池中的区域可以被任何一代抓取甚至在需要时被任何其他堆抓取。因此你不会再看到这样的情况你在gen2或LOH中有一些巨大的空闲空间但它们很长时间都没有被使用如果你的应用程序的行为经历了一些阶段其中一个阶段可能比另一个阶段生存更多的内存而GC认为没有必要做一个完整的压缩GC这种情况就可能发生。在GC工作中我们总是要做出权衡。有了区域我们确实获得了很多灵活性。但我们也不得不放弃一些东西。有一件事使段非常有吸引力那就是我们确实有一个连续的短暂范围因为gen0和gen1总是生活在短暂的段上而且总是紧挨着。当我们在写 屏障中设置卡片时【在GC有一个card tables用来记录对象之间的跨代引用另外就是实现写屏障详细可以翻阅《.NET Core底层入门》P289】我们利用了这个优势。如果你做obj0.f obj1并且我们检测到obj1不在短暂的范围内。我们不需要设置卡片因为我们不需要它只有当obj1比obj0处于更年轻的一代时才需要设置卡片如果obj1不在短暂的范围内这意味着它要么在gen2要么在LOH/POH这些都被逻辑上认为是第二代的一部分但内部被追踪为gen3和gen4我在这篇文章中互换使用LOH和gen3。而这意味着它要么与obj0处于同一代要么处于比obj0更早的一代。) 但是我们只对工作站GC做了这个优化因为服务器GC有多个短暂的范围我们不想在写屏障代码时要和所有的范围进行比较。在区域中我们要么无条件地设置卡片这将使Workstation GC的暂停倒退一些但对Server GC保持相同的性能要么在写屏障中检查obj1的区域这将比在最优化的写屏障类型中检查短暂范围更昂贵。不过区域带来的好处应该比这更有说服力。现在我们可以谈一谈DPAD功能。DPAD是动态升级和降级的意思。严格来说降级已经是动态的了因为它只根据Pin对象的情况动态发生。如果你读过我的备忘录那里解释了降级如果你没读过我强烈建议你读mem-doc。基本上降级意味着一个对象不会像正常情况下那样得到提升。对于段来说降级意味着我们将暂存段的一个范围设置为 降级范围这个范围只能从暂存段的中间一点到该段的末端。换句话说我们永远不会把短暂段中间的一个范围设置为降级范围。这正是因为对于段gen1必须在短暂段的gen0之前在同一个堆上。所以我们不能有一个gen1的部分接着是gen0的部分然后再接着是gen1的部分。升级是GC中一个常见的概念--它意味着如果一个对象存活了一代它现在被认为是上一代的一部分。因此如果你在SOH上有一个长期生存的小对象它最终会被提升到gen2。但这意味着这需要2次GC才能实现。我正计划提供一个API让用户可以选择告诉GC将一个新的对象直接分配到某一代所以你可以将你知道会存活到gen2的对象直接分配到gen2中到目前为止我还没有实现这个API因为有区域的支持也会更容易所以我正计划在我们转换到区域时实现它。但这并不包括所有的情况因为有时用户很难知道一个对象是否会 很可能存活到gen2。而且你可能正在使用一个库对这些对象的分配没有控制。一个非常明显的情况是这种情况会发生在数据基础设施的大小调整上。比方说你或你使用的库分配了一个List它需要增加容量。所以它分配了一个新的T[]对象可以容纳两倍于旧对象的元素数量。现在它为第二部分创建了一堆子元素。现在如果新的数组足够大可以上LOH而且新的子元素都是小对象所以它们在gen0 -通过上文的描述Maoni大佬的团队计划实现一个GC的API可以让用户指定你的对象分配到某一代中默认都是从G0开始。比如我们经常会有这样一些场景我们在程序启动的时候会去读一些数据将它们缓存到内存中这些缓存直到程序关闭才会释放也就是说开发者能知道最终它会到gen2如果没有这个API那么你缓存的对象将从gen0开始经过两次GC才到gen2一般缓存的数据都比较大导致GC在标记和整理过程中会花更多的实际而且可能由于可用内存不足会频繁的去申请空间如果有了这个API开发者就能将对象直接分配到gen2避免了gen0和gen1的GC也避免了频繁扩容空间。(为了说明问题我只展示了一个8元素的数组和4个新的孩子如果这是一个对象[]显然它需要更多的元素才能进入LOH)在片段的情况下我们会看到这样的情况由于新的数组被认为是gen2的一部分这意味着所有在gen0中创建的新元素都将存活到gen2中除非gen2的GC很快发生并发现父数组已经死亡这有可能发生但可能性不大如果真的发生那就非常不幸了因为你花了这么大代价创建一个大对象却马上把它抛弃。但要做到这一点它至少需要经过两次GC。我们很有可能首先观察到一个gen0或gen1的GC这个GC会让这些孩子生存到gen1。然后下一个gen1的GC会发现他们都还活着因为他们被LOH中的那个阵列保持着活力。现在它把它们都提升到Gen2在这种情况下我们更愿意直接将它们分配到gen2。但是这对段来说是很难做到的。我们可以跟踪哪些对象由这些对象组成或者主要由这些对象组成但是当我们做标记时我们不知道哪些对象会一起形成插头【Plug被翻译成插头详情可以看《.NET内存管理宝典》P371和《.NET Core底层入门》P323】。而当我们在形成插头时我们已经失去了这些信息。我们可以在更大的颗粒度上跟踪这些信息。但你猜怎么着这基本上就像区域一样因为我们想把这些信息划分到不同的区域。因为我们想把一个区段划分成更小的单位来跟踪这些信息。所以对于区域来说这是很容易的。当我们做标记时我们确切地知道每个区域上有多少存活下来的东西--当我们标记每个对象时我们跟踪我们需要把存活下来的字节归于哪个区域。所以我们知道有多少存活是由卡片标记完成的。对于区域当我们遇到一个主要由对象组成的区域时如这些因卡片标记而被保留的子对象我们有一个选择我们可以选择将这个区域直接分配到gen2 因此该区域被并入gen2。属于gen0的另一个区域的幸存者被压缩到gen1区域gen0得到一个新的区域用于分配。在目前的实现中我只对那些主要被像这样的对象填满的区域做了这个工作。由于区域很小很可能有些区域被这些东西填满然后我们有另一个区域部分被这些东西填满部分被一些真正的临时对象填满。把它们分开的复杂性是不值得的你可以把它看作是我们回到了这个特定区域的片段情况。当我们这样做时会有一些复杂的情况对于GC来说几乎总是有一些复杂的情况......。一个例子是由于我们现在只是让gen0的对象在gen2中生存我们需要确保如果它们指向任何不是gen2的代就需要为这些对象设置卡片。当我们在重新定位阶段通过活着的对象时我们会这样做因为无论如何我们已经必须通过每个对象。所以双关语部分的意思是这个DPAD功能有点像D-pad......你可以告诉一个区域它需要去哪个方向--向上或向下在GC术语中是指年长或年轻。有很多情况下我们想动态地提升或降低一个区域我上面举的例子只是其中之一。重点是有了区域我们可以动态地指定一个我们希望一个区域最终处于的代数因为代数不再是连续的而且没有特定的顺序代数必须是相对的当然正如你在上面看到的有一些实施细节需要为不同的场景所关注。这比我们以前用分段做的有限的降级要灵活得多。而当我们在GC结束时对区域进行线程化处理时我们只需要将它们线程化到它们所分配的区域。随着我对DPAD的初步检查我已经实现了3个场景我们将动态地促进或降级区域。在未来我们会实现更多。译者注从maoni大佬的这篇文章我们可以看到主流的GC设计都越来越趋于一致了第一眼看到region的时候我就想到了JVM上的ZGC多占用一些内存和牺牲一定的吞吐量来达到亚毫秒级的STW时间而目前看来.NET也在做类似的事情不过我也不敢肯定那么region能为我们带来什么呢有得也有失通常情况下会有更少的内存占用特殊情况下更多的内存占用。因为region的一般来说只有4MB大小而segment会有1GB~4GB大小另外对于pin住的对象segment也不能很好的进行处理从而造成了内存碎片会占用跟多的空间region还有的有点就是释放后会返回到一个池中哪个代需要使用就可以分配给哪个代这比segment模式更加灵活更能复用已申请的内存。为什么会说通常情况下那是因为同样使用1GB内存region的数量肯定比segment要多所以需要有额外的空间来记录region的引用当堆很大(比如TB级别以上)可能会占用更多的内存。更少的STW时间。region很小所以进行标记-整理中整理的步骤时可以将整个region升代加快了整理的的速度。吞吐量的下降。由于region上gen0和gen1不会在连续的地址空间上所以内存屏障付出的代价会更大从而造成吞吐量的下降在此之前.NET的GC都是为吞吐量和P99延时优化的。现在关于DPAD的代码已经合并到main分支中了详情可以看这个PR相信很快就能和我们见面不过看了maoni大佬提交的代码发现个有趣的东西。Region只支持64位操作系统。从下图中的提交来看限定了只有64位操作系统才能使用让我不禁想到ZGC的染色指针通过染色指针来减少写屏障的使用进一步降低STW时间。如果支持了染色指针那么标记可能也会采用三色标记主流的GC算法也趋于一致了。和Hex大佬讨论了一下后面觉得除了azul的C4算法以外.NET GC也有可能会采用CoCo算法来实现CoCo也是一种低延时的算法具体可以看看这篇论文而且已经有人在.NET上实现了这个。如果您想更详细的了解.NET的GC和整个实现的原理您可以看.NET Runtime部分的源码和.NET GC架构师Maoni大佬的博客另外也有两本不错的书推荐。《.NET Core底层入门》由国内精通C 汇编的大佬从2017年阅读CLR源码后编写写的十分详细并且具有极大的参考意义主要是介绍CoreCLR的中间GC的部分也写的很清楚。《.NET 内存管理宝典》由国外研究.NET GC的大佬编写主要围绕着.NET的内存分配、GC执行流程、问题诊断进行介绍是一本不可多得的好书。其它文章Go与C#比较 - 编译、运行时、类型系统、模块和其它的一切Go与C#比较 - 垃圾回收