是否有小伙伴好奇如果没有在代码调用垃圾回收,那么框架会在什么时候调用垃圾回收。本文是读还没出版的伟民哥翻译的 .NET内存管理宝典 - 提高代码质量、性能和可扩展性 这本书的笔记

当前是 2020年9月 本文的知识最新就是当前的时间,因为 dotnet 的更新速度十分快,当前由 dotnet 基金会维护整套 dotnet 开源项目。从编译器到运行时全部都是开源的,采用最友好的 MIT 开源协议,每个项目都会附带完全的构建脚本

在阅读到了伟民哥翻译的 《.NET内存管理宝典 - 提高代码质量、性能和可扩展性》 这本书,我了解到了更多的关于 dotnet 内存的细节,下面请让我给大家分享一下

是否有小伙伴好奇如果没有在代码调用垃圾回收,那么框架会在什么时候调用垃圾回收

在回答这个问题之前需要了解为什么需要进行垃圾回收?这是一个简单的问题,就是咱的内存不是无限量的,需要将不需要使用的内存回收。那么什么是不需要的内存?在 .NET 里面将会给对象分配一定的内存空间,这个类型在不被使用的时候,也就是没有任何代码或线程引用到这个对象的时候,那么这个对象占用的内存就可以回收,因为这个对象不会再被使用

那为什么垃圾回收不是立即的,有一个对象不被使用的时候就回收他的内存?因为框架不知道,一个对象啥时候不被使用是无法在运行时框架立刻知道的,除非是和 C++ 一样手动调用释放内存,或者和 Rust 语言一样对机器友好等。但是如小伙伴所了解这两个语言对开发者不够友好,而对开发者友好的 C# 语言是很难做到这一点,因此就做不到框架立刻知道对象不被使用。所以做不到立刻回收

那么刚才说的 C# 语言很难做到这一点,如果你足够强大,写出的代码能做到这一定,是否就可以立即回收内存?其实也不对,虽然你很强大,但是还有一个坑是内存碎片。内存碎片是因为不同的对象的占用的内存不一样大,而不同的对象被回收的时间不相同,这样就会让一段连续的内存空间,在程序不断使用,被分为很多段。也就是说内存有足够的空闲空间,但是分配不给一个新的对象的需要的空间,因为所有的足够的空闲空间都不连续

因此即使是需要手动释放内存的 C++ 和对机器十分友好的 Rust 语言也都存在这样的问题,在将对象占用的内存释放,还是不够的,需要在合适的时候减少内存的碎片。相对来说,这一点 .NET 的优化会比 C++ 和 Rust 等语言做的好非常多,当然上面这句话也需要看使用的开发者,如果有一个逗比足够逗比,大概有我这么逗比,那么依然可以让 .NET 做的足够渣

刚才为什么说需要在合适的时候减少内存的碎片,而不是说立即?想要回答这个问题,还需要小伙伴有一定的 C++ 基础或 C 的基础,因为在 .NET 系里面,是很难了解到有这样的坑的。在 C 和 C++ 里面最强的就是指针,但是这也是坑的地方。假如我需要减少内存碎片,那么最简单的方法就是压缩内存,压缩的方法就是将所有在使用的对象移动内存空间,让这些对象放在一起,此时空闲的内存空间和在使用的内存空间就分开了,此时也就没有了内存碎片。但是这个方法存在的问题是什么?对象的内存空间地址更改了,而在 C 和 C++ 里面的指针指向的如果是原先的对象的内存地址,在内存压缩时修改了对象的内存地址,这就好玩了,意味着原先的指针都不能使用了。这就是 C 等语言的坑,因为指针也是一个简单的数值,也许会被作为某个变量存放,也许会被作为某个数组里面的元素,或者结构体等使用,因此想要在对象修改内存地址之后,更改完所有的引用的指针是特别难的,因此你无法了解这个值表示的单位是什么,是内存地址还是一个货币。而为什么在 .NET 系里面,是很难了解到有这样的坑,是因为在 .NET 里面不会给你存放某个对象的内存地址,也就是没有简单的指针给你使用。而如果有使用指针,将需要告诉运行时,这个对象被我指针引用了,此时运行时将会帮你固定这个对象,不要去垃圾回收移动这个对象。或者垃圾回收之后可以通过运行时更改对所有的指针

当然了,只要涉及到了 C++ 那将会很复杂,上文说法仅仅只是为了说明 dotnet 垃圾回收的难点,对于 C++ 描述部分是十分片面或者说不对的

继续返回 C# 和 VB 这些语言,因为垃圾回收压缩内存减少碎片修改对象的内存地址对这些高级语言基本没影响,那为什么不立刻执行?原因是有性能影响,在进行压缩回收的时候,需要移动对象,而如果对象的内存移动了,那么就需要更新对这个对象的引用。而如果应用程序还在运行,更新对某个对象的引用,是无法一次性完成的,这就会出现在某些代码访问的还是被移动对象的旧内存空间,而有些代码访问的是被移动对象的新的内存空间。如果此时都是只读,那么没有问题。如果有线程尝试写入就有趣了,如果写入到了对象的旧内存空间,那么相当于没有写入

为了解决这个问题,就需要在进行压缩回收的时候暂停所有的线程,在回收完成才能让线程继续执行。因为线程被暂停了,所以对线程来说好像回收是一瞬间完成的,所有的代码使用的对象的内存空间都被更新了

因为在回收的时候执行压缩回收需要暂停线程,将会降低应用的性能。这就是为什么很多 U3D 游戏在玩家玩的时候都不进行内存回收的原因,假定你在点击开枪的时候,应用进行回收,所有的线程都被暂停,那么你砸不砸桌子

是否间隔一段时间就调用垃圾回收比较好?或者说垃圾回收的时间是多少?其实这个问题是无法回答的,在回答之前先了解设计垃圾回收的决策

• GC应该要经常发生,从而足以避免托管堆包含大量垃圾,导致不必要的内存使用
• GC应该不要太过于频繁地发生,以避免降低性能
• GC应该是高效的。 如果GC只回收了少量的内存,则浪费了性能
• 每次GC应该要很快执行。 许多业务具有低延迟的要求
• 应该能够进行自我调整以满足不同的内存使用模式,开发人员不应该需要知道很多关于GC实现良好的内存利用率的知识,因为很多像我这样的逗比都会自认为了解.NET的内存管理而让实际的GC执行更差

在考虑了上面这些决策,就可以回答垃圾回收的时间是多少这个问题了,假如我的应用程序啥都不做,此时是否还需要回收垃圾?此时不需要。如果我的应用程序是刚好此时空闲了,那么是否在我开始垃圾回收时就开始忙碌了?按照上面的决策可以看到,垃圾回收是尽可能少的调用,以及调用的时候要让垃圾回收执行足够快

想要设计出这样的 GC 方法是十分有难度的,不够世界上强大的开发者很多,现在的 .NET 垃圾回收机制就是艺术品,里面有大量巧妙的设计

如在开源的仓库里面可以看到下面的代码

enum gc_reason
{
    // 小对象分配(AllocSmall)- 在对象分配期间,第 0 代的预算已用完。 这是最常见的情况,在第 0 代分配预算超出的情况下触发
    reason_alloc_soh = 0,
    // 显式诱导GC,没有关于压缩和阻塞的选项
    reason_induced = 1,
    // 操作系统发出内存不足通知信号
    reason_lowmemory = 2,
    reason_empty = 3,
    // 大对象分配(AllocLarge)- 在大对象分配期间,LOH 的预算已用完
    reason_alloc_loh = 4,
    // 慢速路径上的小对象分配(OutOfSpaceSOH)- 在SOH中的“慢速路径”对象分配过程中,分配器空间不足,即使经过一些段重组,甚至可能已经运行了GC,仍然没有所需的可用空间。在具有较大虚拟内存空间的64位运行时中,这应该是一个相当罕见的原因。但是,即使在64位运行时,这种情况也可能发生在工作站GC中
    reason_oos_soh = 5,
    // 慢速路径(OutOfSpaceLOH)上的大对象分配 - 在LOH中的“慢速路径”对象分配期间,分配器空间不足。与OutOfSpaceSOH类似,它应该并不常见
    reason_oos_loh = 6,
    // 没有阻塞的显式诱导GC
    reason_induced_noforce = 7, // it's an induced GC and doesn't have to be blocking.
    reason_gcstress = 8,        // this turns into reason_induced & gc_mechanisms.stress_induced = true
    reason_lowmemory_blocking = 9,
    // 应该进行压缩的显式诱导GC,但仅限SOH,请记住通过其他设置显式启用LOH压缩
    reason_induced_compacting = 10,
    // 主机发出内存不足通知信号
    reason_lowmemory_host = 11,
    reason_pm_full_gc = 12, // provisional mode requested to trigger full GC
    reason_lowmemory_host_blocking = 13,
    reason_bgc_tuning_soh = 14,
    reason_bgc_tuning_loh = 15,
    reason_bgc_stepping = 16,
    reason_max
};

上面代码是放在 C++ 层的运行时,用于运行时使用,当前垃圾回收是基于什么理由。上面代码的具体意思是什么,在伟民哥翻译的 《.NET内存管理宝典 - 提高代码质量、性能和可扩展性》用来几章来讲本文的问题

更详细还需要等伟民哥翻译的 《.NET内存管理宝典 - 提高代码质量、性能和可扩展性》 发布

另外推荐一下伟民哥的 《.NET并发编程实战 - 现代化的并发并行编程模式》(Concurrency in .NET - Modern patterns of concurrent and parallel programming) 这本书


本文会经常更新,请阅读原文: https://blog.lindexi.com/post/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0-dotnet-%E4%BB%80%E4%B9%88%E6%97%B6%E5%80%99%E8%BF%9B%E8%A1%8C%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

如果你想持续阅读我的最新博客,请点击 RSS 订阅,推荐使用RSS Stalker订阅博客,或者前往 CSDN 关注我的主页

知识共享许可协议 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名林德熙(包含链接: https://blog.lindexi.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系

以下是广告时间

推荐关注 Edi.Wang 的公众号

欢迎进入 Eleven 老师组建的 .NET 社区