• CLR内存管理机制与IDisposable对象的GC原理


    目录

    CA1063

    内存

    托管堆

    内存分配

    内存回收步骤

    GC算法

    代数

    后台垃圾回收

    using模式

    IDisposable的正确实现

    析构函数

    GC.SuppressFinalize(this)


    CA1063

    最近写代码遇到了一个code smell CA1063: Implement IDisposable correctly 提示我应该正确实现Dispose模式。什么是IDisposable接口的最佳实现呢?GC过程到底有哪些需要我们注意细节?让我们一起来认识一下C# CLR的内存管理机制与GC原理。

    内存

    我们程序中使用的都是虚拟内存。虚拟内存是为了增加进程的可使用空间,并且无需关心物理内存分配调度问题。操作系统会通过页表管理虚拟内存和物理内存之间的关系,分页可以减少内存资源的浪费。

    虚拟内存被分为三种状态:

    • Free(自由可分配)
    • Reserved(进程保留), 已经分配给进程,但还没分配物理内存,因此无法存储数据
    • Committed(已提交),已经为其分配了物理内存

    托管堆

    通过 C# 值类型与引用类型 实现原理与差异 我们知道引用类型被分配到了"堆"上,什么是"堆"呢?

    GC负责内存管理。当GC被CLR初始化之后,它会为进程分配一段连续的内存空间,这段内存就是我们常说的"托管堆"。每个进程都有一个托管堆, 进程中的所有线程都在同一堆上为对象分配内存,托管堆的大小是可变的。

    托管堆又被继续分为大对象堆和小对象堆,默认大于85000 字节的对象会被放入大对象堆,可以通过runtime config options来配置大对象堆阈值。

    内存分配

    托管堆是一块连续的虚拟内存地址,它自身维护了一个分配指针,指向下一块可用地址。

    分配指针刚开始位于堆顶,每分配一个对象便往后移动一次,对象分配是连续的。

    通常内存分配连续的对象在程序使用中也是连续的,因此这种分配方式可以提高对象访问效率。

    内存回收步骤

    C# GC通过构建对象引用图的方式来决定哪些对象需要回收。它会从一系列""出发,查找正在被引用的对象。未被引用的对象会被标记为无法访问,并回收为它们分配的内存。

    大家可以想象,分配指针不断的在托管堆的末尾分配空间给新的对象,而之前旧的对象又零零散散的被回收,那么原本一块连续的托管堆最后会变得千疮百孔。

    因此GC还会负责托管堆的"压缩"工作,即当GC发现大量对象被回收时,便会将后边的对象复制到前边的地址空间来,挤压掉托管堆中间的空隙。这样可以提高空间利用率,也有利于较大对象的分配。

    到这里,你大概就能明白为什么要设置大对象堆和小对象堆了,大对象堆为了避免对象移动,通常不会压缩内存。

    哪些对象可以作为根?静态字段、局部变量、CPU 寄存器、GC 句柄和终结队列。

    GC算法

    GC算法遵守以下原则: 

    • 压缩托管堆的一部分内存要比压缩整个托管堆速度快。
    • 较新的对象生存期较短,而较旧的对象生存期则较长
    • 较新的对象趋向于相互关联,并且大致同时由应用程序访问。

    代数

    结合以上三个原则,托管堆将对象分为了0,1,2 三代。所以每次在GC时只需要维护部分堆空间,而不是压缩整个托管堆的内存。

    第0代是GC频率最高的,刚创建的对象都会被分配到0代堆或者大对象堆。当0代堆空间不足的时候便会触发0代GC,大多数对象都会被回收,而那些幸存下来的对象就会"升级"成为1代。

    第1代对象堆满了也会触发GC,那些幸存下来的对象成为第2代。

    大对象因为是被单独维护的,它们属于第2代对象,有时也被称为第3代,但它们会和第2代对象一起被回收。CLR会尽量维持GC频率和不同代堆空间大小之间的平衡,以求获取最大的时空效率。

    后台垃圾回收

    在.NET Framework 4之后使用后台垃圾回收机制机制。0代和1代对象被称为暂时代,对暂时代的垃圾回收始终是阻塞式的,即所有线程都将被挂起,在GC完成之后才能继续工作。因此,暂时代如果分配的对象过多将会严重影响程序性能。

    using模式

    了解IDisposable接口前,先看两个简单的问题:

    1. Stream,SqlConnection是不是非托管对象?不是。它们是托管对象,也会被分配到托管堆上。

    2. 为什么我们要用using来声明它们呢?因为它们会占用数据库链接,文件等非托管资源,我们知道GC的触发时机是不确定的,因此使用using是为了及时释放这些资源。而using的原理就是会调用对象的Dispose()方法,主动释放对象占用的资源。

    IDisposable的正确实现

    了解C# CLR的内存管理机制后,我们再来看这个问题:如何正确实现IDisposable接口。

    1. 首先要明确一点:GC在回收资源时,只会调用对象的析构函数,不会调用Dispose方法。
    2. Dispose方法应该是幂等的,其不应引发任何异常,  非托管资源只能被释放一次。道理很简单,你申请了一块非托管内存,并在之后释放了它,那么它就有可能被重新分配给其他程序使用,如果再次去释放它就错了。

    来看一个正确的例子,我们注意到对象除了实现无参Dispose方法之外,还声明了一个有参的Dispose虚方法,而这个虚方法才是资源释放的真正实现。

    1. public class ManagedAndUnmanagedObject : IDisposable
    2. {
    3. private SqlConnection sqlConnection = new SqlConnection();
    4. private UnmanagedHandle unmanagedHandle = Win32.SomeUnmanagedResource();
    5. private bool disposed;
    6. // Dispose方法由用户手动调用或者using模式使用
    7. public void Dispose()
    8. {
    9. Dispose(true);
    10. GC.SuppressFinalize(this);
    11. }
    12. protected virtual void Dispose(bool disposeManaged)
    13. {
    14. // disposed字段标记该对象是否已经被释放,重复释放会引发异常
    15. if (!disposed)
    16. {
    17. if (disposeManaged)
    18. {
    19. if (sqlConnection != null)
    20. {
    21. //级联释放调用
    22. sqlConnection.Dispose();
    23. }
    24. }
    25. unmanagedHandle.Release();
    26. disposed = true;
    27. }
    28. }
    29. // 析构函数供GC调用,如果没有非托管资源,则可以省略
    30. ~ManagedAndUnmanagedObject()
    31. {
    32. Dispose(false);
    33. }
    34. }

    析构函数

    那么为什么Dispose方法调用它时要传true,而析构函数在调用时要传入false呢?

    这是因为析构函数只应该负责非托管资源的释放。GC在回收对象前会调用对象的析构函数,假如用户没有调用Dispose方法来主动释放非托管资源,那么实现析构函数可以保证非托管资源在最后能被正确释放。

    GC.SuppressFinalize(this)

    GC.SuppressFinalize(this) 则是为了告诉GC,不要在回收该对象的时候调用析构函数。因为根据上面的实现,当我们手动调用Dispose的之后,非托管资源已经被释放掉了。

    有些时候,我们可能没有用到非托管资源,但仍需要实现 IDisposable 接口来做一些事情,这种情况下可以不要析构函数,GC.SuppressFinalize(this) 也就不必要了。

    在C# 8.0之后出现了 IAsyncDisposable接口,原理是一样的。

  • 相关阅读:
    Qt QCustomPlot介绍
    PyCharm运行PyQT6 (四) 百篇文章学PyQT
    JdbcTemplate查询操作
    三维图形学知识分享--三角剖分网格细分详细代码实现
    蓝桥杯官网练习题(回文日期)
    一键抓取网页的所有图片
    42.(后端)更新用户信息
    Vue2响应式原理分析(数据代理与数据劫持)
    什么是重绘和回流(重排)?什么情况下会用到?如何减少
    SQLite:TIMESTAMP类型使用
  • 原文地址:https://blog.csdn.net/qq_40404477/article/details/126312637