争辩:读完最后,您将了解GC的工作方式和原因

我会马上说:我从不等待对这个关于社会保障问题的详细回答。 这是愚蠢的,对我而言是自私的。 但是,我认为,除了对该平台的普遍兴趣外,了解它的工作原理也非常有用,因为 这消除了许多问题。 例如,当开发人员认为自动调用Dispose并且您不需要自己调用它时,它将排除该选项。 或者,如果开发人员更有经验,则可以在肌肉记忆水平上自动帮助他,编写导致问题最少的代码。


我主观上不喜欢的另一个问题是如何解释他的作品。 因此,我提出了我的书.NET Platform Architecture中描述的另一种方法。


如果我们要彻底理解为什么选择了这两种内存管理算法:Sweep和Compact,我们将不得不考虑世界上存在的数十种内存管理算法:从普通字典开始,以非常复杂的无锁结构结束。 取而代之的是,我们不去考虑什么是有用的,而是简单地证明了选择的合理性 ,从而了解了为何如此选择。 我们不再看助推器发射手册:我们掌握了一整套文档。


争端是互惠互利的:如果不清楚,我将更正本书中的不清楚之处,其中一小部分是给定的文本。



我选择了推理的格式,这样您就可以感觉到平台的架构师和我本人得出的结论与真正的架构师在Redmond的Microsoft总部得出的结论相同。

根据已分配对象的大小(基于其大小)分类,您可以将内存分配空间划分为两个大部分:一个对象大小小于某个阈值的地方和一个大小大于该阈值的地方,看看在管理这些组方面有什么不同(基于他们的大小)以及它的结果。


如果我们考虑管理常规的“ ”对象,我们可以看到,如果我们坚持存储有关每个对象的信息的想法,那么对于我们而言,维护将存储指向每个此类对象的链接的内存管理数据结构将非常昂贵。 最后,可能会发现,为了存储有关一个对象的信息,您将需要与对象本身占用的内存一样多的内存。 相反,您应该考虑:如果在垃圾回收期间我们从根开始跳舞,通过对象的传出字段深入图形,并且仅需要沿着堆的线性通道来识别垃圾对象,那么是否有必要在内存管理算法中存储有关每个对象的信息? 答案很明显:没有必要这样做。 因此,我们可以尝试从不存储此类信息这一事实着手:我们可以线性地遍历一堆,知道每个对象的大小,然后每次将指针移动下一个对象的大小。


堆上没有其他数据结构,这些数据结构保存指向堆控制的每个对象的指针。

但是,当我们不再需要内存时,必须释放它。 当释放内存时,我们变得难以依靠堆的线性通道:它很长且无效。 结果,我们得出的结论是,我们需要以某种方式存储有关空闲内存区域的信息。


堆具有可用内存列表。

如果按照我们的决定,如果要存储有关空闲区域的信息,并且在释放内存的同时这些区域太小,那么我们首先会遇到一个相同的问题:存储有关在考虑占用空间时遇到的空闲区域的信息(如果在占用的对象的侧面释放对象,以便存储有关它的信息,在最坏的情况下,有必要将其大小的2/3释放(指针+大小与SyncBlockIndex + VMT +一些字段-对于对象)。 您必须承认,这听起来还是很浪费:释放一群彼此跟随的物体并不总是好运。 通常,它们以混乱的方式释放。 但是与繁忙的站点不同,我们不需要线性搜索,而需要搜索空闲站点,因为分配内存时,我们可能会再次需要它们。 因此,出现了一种完全自然的愿望,即减少碎片并挤压堆,将所有占用的区域移动到空闲位置,从而在空闲区域中形成可以分配内存的较大区域。


这就是压缩算法思想的来源。

但是等等,你说。 毕竟,此操作可能非常困难。 想象一下,您在堆的最开始就释放了一个对象。 您说什么,您需要移动所有东西吗? 好吧,当然,您可以幻想一下CPU的矢量指令,可以用来复制很大的内存区域。 但这仅仅是工作的开始。 我们还必须将所有指针从对象字段固定到发生移动的对象。 此操作可能要花很长时间。 不,我们必须从其他角度出发。 例如,通过将堆内存的整个段划分为多个扇区并分别使用它们。 如果我们在每个扇区中分别工作(为了可预测性和扩展此可预测性,最好是固定大小),则压缩的想法似乎并不那么沉重:压缩单个扇区就足够了,然后您甚至可以开始谈论压缩一个这样的扇区所需的时间。 。


现在,仍然需要了解在什么基础上划分部门。 在这里,我们必须转向平台上引入的第二种分类:基于其各个元素的生存时间的内存共享。


划分很简单:如果考虑到随着地址的增加我们将分配内存,那么最先选择的对象将成为最早的对象,而位于高级地址中的对象将成为最年轻的对象。 进一步,您可以得出明智的结论:在应用程序中,对象分为两类:为长期使用而创建的对象和为生存很少而创建的对象。 例如,以集合的形式临时存储指向其他对象的指针。 或相同的DTO对象。 因此,不时地挤压一堆,我们在高级地址中得到了许多长寿命的对象-在低位地址中有许多短寿命的对象。


这样我们就收到了几代人

将记忆分为几代人,我们有机会更少地关注越来越多的老一辈对象。


但是,另一个问题出现了:如果我们只有两代人,我们就会遇到问题。 否则,我们将尝试快速实现GC的无掩饰工作:然后,将年轻一代的大小缩小到最小。 结果,对象将在较早的一代中意外失败(如果GC“在许多对象的内存分配期间立即工作”)。 或者,为了最大程度地减少意外故障,我们将增加年轻一代的人数。 然后,年轻一代的GC将工作足够长的时间,从而减慢并减慢了应用程序的速度。


解决方案是引入“中间代”。 十几岁 换句话说,如果您到了青春期,则您更有可能存活到老年。 其引入的本质是在最小的年轻一代最稳定的老一代之间取得平衡,最好不要碰任何东西。 这是尚未决定物体命运的区域。 第一代(不要忘记我们从头开始的想法)也被创建得很小,并且GC在那看起来不那么频繁。 因此,GC允许临时的第一代对象无法进入较难收集的较早的对象。


因此我们有了三代人的想法。

优化的下一层是尝试拒绝压缩。 毕竟,如果您不这样做,我们将摆脱繁重的工作。 让我们回到免费网站的问题。


如果在用完堆中所有可用的内存并调用了GC之后,自然会希望拒绝压缩,以便在释放的段的大小足以容纳一定数量的对象的情况下进一步在释放的段内分配内存。 在这里,我们想到了第二种在GC中释放内存的算法,称为Sweep :我们不压缩内存,我们使用释放对象中的空隙来放置新对象


因此,我们描述了并证明了GC算法的所有基础知识。

因此,两天后,我们可以得出一些结论。 据我了解,大多数人都能理解大部分文字,甚至全部文字。 有些人回答说他们不理解,有些人则部分理解了。 正如他们所说,这一争端是由一群读者赢得的,尽管只有一点点余地。 但是,正如我所说,每个人都会从中受益:案文将得到修改和补充。 另外,在两个地方都进行了更新:书中和本文中都进行了更新。


Source: https://habr.com/ru/post/zh-CN464169/


All Articles