找回密码
 立即注册
首页 业界区 安全 简单讲讲.NET GC垃圾回收的“分代处理,标记、清除、压 ...

简单讲讲.NET GC垃圾回收的“分代处理,标记、清除、压缩”

蟠鲤 2025-8-13 20:13:17
GC垃圾回收

GC垃圾回收是.NET(CLR)运行时的核心组件,其控制堆中的内存分配与回收,减轻开发者手动管理内存的负担。避免内存泄露和悬空指针(对象内存被释放后仍被访问)等问题。
其核心基于分代收集和标记-清除-压缩算法。
如果前面看着枯燥,且压缩机制还未弄清除。可直接跳转到下方压缩标题,也许会给你些新的思路。
GC触发机制

当第0代内存分配容量超出阈值,则会触发垃圾回收。阈值动态调整,通常为几mb。
GC可以手动触发,调用GC.Collect();静态函数。(通常在调试或场景切换时使用)
混合触发,调用GC.AddMemoryPressure();
混合触发会提示GC当前非托管内存压力增大,但是否触发回收由GC策略决定(基于托管堆占用率、碎片率等综合判断)
使用混合触发时如果托管堆内存充足,GC可能延迟回收。
分代收集

GC会根据分代收集将堆中的对象分为3代,Gen 0、Gen 1、Gen2。还有一个独立存放大占用对象的大对象堆LOH。
在对象刚开始创建时会将其放入第0代内存,当0代内存存储数据容量超出阈值(动态调整,通常256kb-4mb)时,会触发GC。
GC会把0代内存中仍被引用的对象标记为存活,未被引用的对象标记为垃圾。GC将垃圾对象清除释放空间。
然后将0代中所有(非固定对象)标记为存活的对象复制到下一代内存中(如果是0代就复制到1代中,如果是1代就复制到2代)
复制完对象到下一代内存中后,其原本所在的内存空间会被回收。
这里要注意,如果对象占用空间大与等于85kb(.NET8中LOH阈值提升至100kb),那么该对象会直接存入大对象堆LOH中。
堆段GC调用Gen 0  新建对象占用小于等于85kb(.NET8中LOH阈值提升至100kb)的对象存入该内存。Gen 0存储数据容量超出阈值时GC调用。Gen 1 GC调用时0中被引用的对象会被复制到该内存。在0代垃圾回收后,会判断Gen 1中的对象和Gen 0中的对象占用容量之和是否大于了Gen 1的动态阈值,如果超过了那么Gen 1就会调用GC,其调用GC时Gen 0也会被GC连带检查。Gen 2 GC调用检查Gen 1内存时会将Gen 1中被引用的对象复制到该内存Gen 2调用GC时所有内存0、1、2、LOH都会被GC检查处理。LOH 新键对象占用大于85kb(.NET8中LOH阈值提升至100kb),直接放入LOH中。自身不会触发GC,只在Gen 2触发GC时,才会被连带处理(无论LOH空间是否已满)。就算手动调用GC也不会回收LOH。每代内存都会在内存中划分自己的地址范围,其中0代与1代的地址划分是严格连续的。
如果0代占0x0000-0x1000,1代就一定占0x1001-0x2000。
2代与LOH是独立堆段,其地址划分与0/1内存无连续性。
压缩

为什么会有压缩?首先我们需要讲讲内存碎片。
我们在将对象存入2代或LOH内存时都是逻辑连续的(包含内存间隙),我们可以将各个对象看成是一个个在键盘中挨着的键帽。
在未清理时,每个键帽都是挨着的,但是在调用GC后,发现有些键帽坏了,那么GC会将那些键帽清除。
而这时我们再来看键帽,已经不再是密集一个个排列的了,他们中间空出了坏键帽的位置。这些空出的位置就是内存碎片。
在这时我们新加入了一个键帽,如果内存碎片中有一个的容量足以放入新键帽那该内存碎片就有可能被再次填上。
但是如果该对象的占用比空出位置小呢?那么就会产生一个更小的内存碎片。这个碎片有可能很难再放入新加入的键帽(对象)了,那么这个碎片就很可能完全被浪费。
就算这时还有能存放一些一般容量大小的碎片,但这种碎片一多,多到到处都是,到处都是键帽隔着碎片,已经没有空余的大空间存放像是回车删除Shift这样比正常键帽要大的键帽了,
但这时可存储容量却显示仍然够用,这时候来了一个回车,回车进来一看,虽然可存放空间仍有富裕,但是那只是内存碎片的总容量,而现在没有一个碎片能放下回车。这时就出问题了,所以我们引用了压缩算法。
压缩就是在内存碎片过多时,将各个对象移动,让他们再次变成一个连续的结构。这样原本存在的一个个小空位就消失了。
Gen 0内存和Gen 1内存中的对象是不进行压缩的。其直接复制对象到新地址。所以不会有内存碎片
Gen 2中的对象可能会压缩,其会根据Gen 2中的碎片率来决定是否压缩。
LOH从.NET4.5.1版本开始可以通过显示设置强制压缩
  1. //设置LOH压缩模式,LOH压缩会在下次Gen 2触发GC时进行压缩。
  2. //LOH压缩执行完,标识符会自动重置。如果想要LOH再次压缩须重新设置
  3. GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
复制代码
压缩会更新其所在堆段中的所有对象地址,而要实现更新则需要使用Stop-The-World将所有用户线程暂停。
也就是在压缩时,应用程序会整个暂停。在用户的视角下不如说更像是卡顿。
POH固定对象堆.NET 5+新特性
  1. //创建固定对象,不会被GC移动
  2. var pinnedObj = GC.AllocateArray<byte>(size: 1024, pinned: true);
复制代码
以这种方式创建的对象,不会被GC移动。就算是位于0代内存或1代内存也不会被GC在释放内存迁移对象到下一层时移动。
比如固定对象创建在了0代内存中,这时触发垃圾回收。其他非固定且被引用的对象会被迁移到1代内存中,而固定对象会固定在0代内存不会被迁移。
但我们也可以明显看出这种形式对象的弊端,它会阻碍压缩操作,可能加剧内存碎片。需谨慎使用。且固定对象使用完须及时释放。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册