C# 的垃圾回收机制(GC)

概述

C# 的主要优势之一是自动内存管理,开发人员无需手动释放未使用对象的内存,提高了开发效率。当然,这不能避免一些内存泄漏与程序崩溃的相关问题。除了在 C# 中避免垃圾回收问题外,Unity 中内存管理实现的特殊性也产生了额外限制。

需要注意的是,C#只是一门语言,VM才是运行它的环境,所以Unity与其他.NET应用运行时的部分内容(比如GC)会有一定差异。

.NET的GC

.NET CLR 中的GC算法基于多个考虑因素:

  • 新对象的生命周期一般较短,而旧对象的生命周期较长;
  • 压缩一小部分托管堆的内存比一次性压缩整个托管堆的内存要快;
  • 新对象通常是相互关联的,并在大致相同的时间提供给应用程序;
  • 基于这些久经考验的假设,CLR 垃圾收集器的算法如下,有三代对象:

第 0 代:所有新对象都进入这一代;
第 1 代:第 0 代中的对象如果在一次垃圾回收中存活下来,就会被移入这一代;
第 2 代:第 1 代中的对象如果在第二次垃圾回收中存活下来,就会被移入这一代;

上述假设的意思就是,新的对象都会经过第 0 代并且经过垃圾回收期的筛选,而能留到第 2 代的,往往是整个应用程序生命周期都能用得上的持久对象,而他们需要被回收(没有被引用)的概率也会低得多。

每一代在内存中都有自己的地址空间并独立于其他代进行处理。当应用程序启动时,垃圾回收器会将所有创建的对象放入第 0 代的空间。一旦没有足够的空间放置其他对象,就会触发并开始第 0 代的垃圾回收。

那么,如何确定不再使用的对象呢?为此,垃圾回收器会使用应用程序的根列表(list of roots)。这个列表通常包括:

  • 类的静态字段;
  • 局部变量和方法参数;
  • 存储在线程栈的对象的引用;
  • 存储在处理器寄存器中的对象的引用;
  • 等待最终确定的对象;
  • 与事件处理程序相关的对象也可能包括在根列表中;

垃圾回收器会分析根对象列表,并从根对象开始建立一个可访问对象图,无法从根对象访问抵达的对象被视为死对象(就是常说的垃圾),垃圾回收器会释放这些对象占用的内存。

处理完 0 代的死对象后,垃圾收集器会将剩余的对象转移到 1 代的地址空间。一旦进入第 1 代,垃圾收集器就不会再频繁地考虑删除这些对象。而随着时间的推移,如果第 0 代的清理工作无法为创建新对象提供足够的空间,就会执行第 1 代的垃圾回收。然后再次构图,再次删除死对象,再把第 1 代中存活的对象移至第 2 代。

如果持续发生内存泄漏,垃圾回收器将耗尽所有三代可用地址空间,并决定向操作系统请求额外空间。如果内存泄漏持续存在,操作系统将继续为进程分配额外的内存,直至达到一定限度。当可用内存耗尽时,操作系统将被迫停止进程(崩溃)。

同时 .NET 区分小对象堆(SOH)和大对象堆(LOH)。对于大对象的判断阈值是对象占用内存大于85000字节。并非所有新创建的对象都直接是 G0(第一代)的,大部分的小对象创建的时候是G0,但是对于大对象创建的时候直接被加入到LOH,这部分是从G2切割一部分空间来存储的。而大对象堆里面的对象也是要在G2的GC阶段才能被触发。

由于分代垃圾回收的设计,对于临时的创建的小对象,用一次就扔的,能在G0 的GC阶段快速扫描并释放。而 .NET的设计是G0只扫描标记为G0的对象集合。G1的扫描会同时扫描G0和G1标记的对象集合。G2的扫描会同时扫描G0、G1、G2的集合。所以很显然对于那种大对象,比如装了很多很多数据的数组,字典等,尽量是重用它们而不是随意扔掉。因为即便是新new出来的它们依然会直接进入到G2。

返回一个大的数组的方法,比如GetVerticesList或者GetColorList之类的函数,返回了一个用一次就扔掉的List,对GC性能是有比较严重的损耗的。所以如果频繁地创建和销毁大对象,特别是类似GetXXX列表的,好的设计应该是设计成 bool GetXXXList(List outResult) 而不是设计成 List GetXXXList()。

Unity 的 GC

要注意的是,Unity 的GC系统甚至整个底层相对 .NET 已经很落后了(这也是为什么很多开发者希望 Unity 支持高版本 .NET 框架)。

Unity 使用保守的 Boehm-Demers-Weiser(BDW)垃圾回收器,它会停止程序的执行,只有在完成工作后才会恢复正常执行。BDW 的工作算法可描述如下:

  • 停止世界(Stop the world):垃圾收集器会暂停程序执行,以便进行GC;
  • 根扫描:垃圾收集器首先会识别 Root,即程序可直接访问的变量,这些变量可能指向堆上的对象。这些根是垃圾收集程序的起点;
  • 标记阶段: 标记阶段从根集合开始遍历,跟随所有可到达的指针,标记当前正在使用的所有对象。该算法使用位图(bitmap)或其他数据结构来跟踪哪些对象已被标记;
  • 保守扫描:由于BDW不需要修改应用程序的代码,因此它必须能够处理指针可能存储在对象内任意位置的情况(如实际保存地址的整数)。这就是它被称为“保守”的原因;它假定内存中任何字大小的模式都可能是指针,从而谨慎行事;
  • 指针旋转:为了确保在收集过程中移动的对象不会被指针覆盖,垃圾收集器可能会使用一种称为指针旋转的技术。这包括临时修改即将被移动的对象的指针,以免它们在收集过程中丢失;
  • 清扫阶段:标记完所有可访问对象后,就开始进入扫描阶段。收集器会扫描堆,找出尚未标记为正在使用的对象。这些对象被视为垃圾,可用于收集;
  • 写屏障: 为了在垃圾回收过程中保持对象图的完整性,Unity会使用写屏障。这是在程序写入堆对象时执行的检查。如果写入可能会改变图结构(例如,为对象创建一个新引用),写入障碍就会确保垃圾收集器知道这一变化;
  • 释放: 一旦清扫完成,垃圾收集器就可以为正在收集的对象运行释放相关的代码。这可能包括释放文件句柄或网络连接等资源的清理代码;
  • 恢复世界(World resumption):垃圾回收完成后,程序继续执行;

对于标记和扫描阶段,更易懂的描述是:先遍历全局的对象的指针列表把对象标记为不可达,然后找出根集合标记为可达,再通过根集合引用做递归追踪,被追踪到的标记为可达。这过程是有向广度遍历。这一步做完后如果还是处于不可达状态则需要回收。

这是一种保守的垃圾回收,也就是说,它不需要关于内存中所有对象指针位置的精确信息。相反,它认为内存中任何可能是对象指针的值都是有效的指针。这样,BDW 垃圾收集器就能与不提供精确指针信息的编程语言协同工作。

另外,由于使用了不支持内存压缩(内存压缩是指将不连续的内存压成连续的内存)的保守型垃圾回收器,Unity 中的内存碎片问题尤为突出。如果不进行压缩,释放的内存块就会保持分散状态,从而导致性能问题,运行周期越长问题越明显。下图生动地描绘了内存碎片的存在:

并且要注意的是,由于Unity GC并不是分代式的GC,在性能上有一定影响。因此从 Unity 2019 开始,BDW 默认启用增量式GC(Incremental garbage collection)。这意味着垃圾收集器会将其工作量分配到多帧中,而不是直接停止 CPU 主线程(阻断主线程)来处理托管堆中的所有对象。

因此,Unity 在执行应用程序时会有较短的掉帧时间,而不是卡住一段时间,从而允许垃圾回收器处理托管堆中的对象。增量模式并不会从整体上加快GC的速度,但由于它会将工作量分配到多个帧中,因此与GC相关的性能峰值会有所降低。

但是增量式GC可能会有些问题。在这种模式下,垃圾回收器会进行分帧操作,包括标记阶段。标记阶段是垃圾回收器对所有托管对象进行扫描,以确定哪些对象仍在使用,哪些需要清理。如果对象之间的大多数引用在工作片段之间不发生变化,那么分帧的标记阶段就会很有效。但是,频繁改变引用会使增量式垃圾收集器超负荷,造成标记阶段永远无法完成的情况。在这种情况下,垃圾收集器将转而执行全面的非增量收集。为了通知垃圾收集器每次更改引用的变化,Unity 会使用写屏障(Write Barriers);这会在更改引用时增加一些开销,从而影响托管代码的性能。

.NET GC 与 Unity GC 的比较

参考

https://docs.unity3d.com/cn/2021.1/Manual/overview-of-dot-net-in-unity.html

https://medium.com/my-games-company/memory-mastery-comparing-unity-and-net-garbage-collection-4c23e693d3a5

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇