代码编织梦想

是否有小伙伴好奇如果没有在代码调用垃圾回收,那么框架会在什么时候调用垃圾回收。本文是读还没出版的伟民哥翻译的 .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/ 欢迎大家访问,里面有很多新的博客。只有在我看到博客写成熟之后才会放在csdn或博客园,但是一旦发布了就不再更新

如果在博客看到有任何不懂的,欢迎交流,我搭建了 dotnet 职业技术学院 欢迎大家加入

如有不方便在博客评论的问题,可以加我 QQ 2844808902 交流

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

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接: https://blog.csdn.net/lindexi_gd/article/details/110531842

非技术 在线教育与电信行业的发展-爱代码爱编程

今天在去买菜的时候,听到旁边有个移动的小哥哥在向一位大叔叔推荐办理移动带宽升级业务,他说到一句话,你升级一下带宽,你的小孩上在线课程的时候多顺哇。本来是没当回事,只是无聊的时候想到这一点,我认为这里是一个大的利益链,于是写博客记一下 现在是 2020.08.30 我想看看是否我认为的是对的 本文不聊任何技术,也不聊任何音视频相关的技术哈,只是想聊聊利益

阿里巴巴给全体Java程序员最好的“礼物”:Java架构成长笔记,由浅入深,从0到1-爱代码爱编程

导言 提起阿里,行外人联想到的关键词无非是“交易”、“淘宝”、“支付宝”,但对于程序员来说,阿里庞大的技术体系才是最吸引人的。实际上阿里作为国内一线互联网公司的头把交椅,内部的技术体系和发展都是备受关注的,对于程序员来说,能够进到阿里工作,就是对自己的技术水平进行一个提升和学习。 实际上,阿里内部的技术交流氛围是极其强烈的,技术人员也经常会交流自己的学

dotnet 使用 Obsolete 特性标记成员过时保持库和框架的兼容性-爱代码爱编程

在开发库以及框架的时候,持续维护会遇到兼容性的问题,如发现了旧版本有一些接口设计不合理,或者方法命名不符合逻辑等。此时如果直接更改原有的属性名或方法名甚至类名等,将会导致上层业务的开发者们在升级库之后构建不通过,因为缺少对应的方法。此时就需要上层业务的开发者们查阅文档才能了解如何应对升级之后带来的变动 在 dotnet 里面,可以使用 Obsolete

使用 IOC 控制反转和 DI 依赖注入的意义-爱代码爱编程

其实我的标题没写对,这个话题我是聊不下去的。 本文只和小伙伴聊聊为什么使用容器注入,优缺点是什么。我通过问问题的方式让小伙伴了解这么做的意义 在开始之前我就可以告诉小伙伴使用容器注入的缺点了,尽管这很有争议 内存泄漏降低性能那是否 IOC 控制反转意味着一定需要配合 DI 依赖注入?答案是不一定的,还有好多有趣的手段 那 DI 依赖注入和容器注入有什

成为Java架构师除了掌握Java语法,还要系统学习哪些Java相关的技术?-爱代码爱编程

有不少朋友问,成为Java架构师除了掌握Java语法,还要系统学习哪些Java相关的技术,今天分享一个,互联网Java技术学习路线图。 一、构成架构师的技能体系   二、阅读源码,分析源码知识点总汇   这张图详细介绍了源码中所用到的经典设计思想及常用设计模式,先打好内功基础,了解大牛是如何写代码的,从而吸收大牛的代码功力。 结合Spr

Java架构师8年开发笔记整理汇总-爱代码爱编程

为啥要分享? 在写代码的路上,我们都遇到过很多的艰难险阻,遇到过很多自己没有办法解决的问题,接受过别人的帮助,  到现在既然有这个能力了,我也想去帮助他人! Java架构师 应该算是一些Java程序员们的一个职业目标了吧。很多码农码了五六年的代码也没能成为架构师。那成为Java架构师要掌握哪些技术呢,总体来说呢,有两方面,一个是基础技术,另一

dotnet OpenXML 文本字体的选择规则-爱代码爱编程

在 Office 的文本排版里面,会根据字符选择使用哪个字体插槽。也就是实际上在 Office 里面可以在一个文本段里面指定多个字体,会根据实际的字符使用不同的字体 在做 Office 解析的时候,在 OpenXML SDK 里面是没有找到表示字体的属性的,只能找到 LatinFont 和 EastAsianFont 和 ComplexScriptFon

WPF 在后台代码定义 ResourceDictionary 资源字典-爱代码爱编程

在 WPF 中的 ResourceDictionary 资源字典大部分都是在 XAML 里面定义的,但是在 C# 代码定义一个资源字典也是可行的,只是写起来有点诡异 在 CSharp 后台代码里面给 WPF 定义资源字典需要重新创建一个类,让这个类继承 ResourceDictionary 如以下代码 public class Foo :

asp dotnet core 提供大文件下载的测试-爱代码爱编程

本文仅仅是提供测试使用的代码 提供文件下载只需要返回 PhysicalFile 方法,如下面代码 [HttpGet] public IActionResult Get() { var folder = Path.GetDirectoryName(Assembly.GetExe

WPF 底层 从手指触摸屏幕到笔迹在屏幕显示中间的步骤-爱代码爱编程

整个 WPF 就是一个UI框架,一个 UI 框架最重要的是 交互 和 显示 部分,而书写这个功能将会完全贯穿 WPF 整个框架的功能。本文非入门级博客,本文包含了大量链接博客,阅读本文你将会了解从用户手指触摸屏幕到最终屏幕打印出笔迹的应用程序执行的步骤 本文实际内容不多,但是如果加上链接的博客,那么总内容将会非常多,还请小伙伴仔细阅读本文链接的博客 从

WPF 设置资源字典多线程安全读写方法-爱代码爱编程

在 WPF 中,使用 ResourceDictionary 本身不会受到创建线程同步影响,意味着可以在任意的线程创建 ResourceDictionary 资源字典,然后在任意线程使用。但是此时的读写需要有时间上的差距,否则将会多线程读写不安全。在 ResourceDictionary 有一个 CanBeAccessedAcrossThreads 属性用来

WPF dotnet core 如何开启 Pointer 消息的支持-爱代码爱编程

在 WPF 下,可以使用和 UWP 一样的 Pointer 触摸架构,只是开启的方式和 .NET Framework 版本有细微的差异 看过 win10 支持默认把触摸提升 Pointer 消息 的小伙伴可以了解到,这个博客的方法是通过配置文件的方式 而在 .NET Core 的 WPF 下是不会去读取 App.config 文件,那么此时应该如何开启