说明 GC 之前先讲解下虚拟内存

# 虚拟内存
  • 一个系统如果同时运行着很多进程,为各进程分配的内存之和可能会大于实际可用的物理内存,虚拟内存管理使得这种情况下各进程仍然能够正常运行。因为各进程分配的只不过是虚拟内存的页面,这些页面的数据可以映射到物理内存页面,也可以临时保存到磁盘上而不占用物理内存页面,在磁盘上临时保存虚拟内存页面的可能是一个磁盘分区,也可能是一个磁盘文件,称为交换设备。
    当物理内存不够用时,将一些不常用的物理页面中的数据临时保存到交换设备,然后这个物理页面就认为是空闲的了,可以重新分配给进程使用,这个过程称为换出。如果进程要用到被换出的页面,就从交换设备再加载回物理内存,这称为换入。换出和换入操作统称为换页,因此:系统中可分配的内存总量 = 物理内存的大小 + 交换设备的大小。
  • 虚拟内存有三种状态,Free、Reserved、Committed
# 什么是托管堆?

在基于 Net 框架的应用程序中初始化新进程时,CLR 在初始化之后会为进程保留一个连续的地址空间区域,用于存储和对象管理,这个保留的地址空间被称为托管堆,分配的内存就是上面提到的虚拟内存,托管堆分为小对象堆和大对象堆,大对象堆包含不少于 85000 个字节的对象,这些对象通常是数组。

# Net 框架中的内存分配与释放
  • 内存分配

托管堆中维护着一个指针,用它指向将在堆中分配的下一个对象的地址。
最初,该指针设置为指向托管堆的基址。 托管堆上部署了所有引用类型。 应用程序创建第一个引用类型时,将为托管堆的基址中的类型分配内存。 应用程序创建下一个对象时,垃圾回收器在紧接第一个对象后面的地址空间内为它分配内存。 只要地址空间可用,垃圾回收器就会继续以这种方式为新对象分配空间。

从托管堆中分配内存要比非托管内存分配速度快。
由于运行时通过为指针添加值来为对象分配内存,所以这几乎和从堆栈中分配内存一样快。 另外,由于连续分配的新对象在托管堆中是连续存储,所以应用程序可以快速访问这些对象。

  • 内存释放

.NET 的垃圾回收器管理应用程序的内存分配和释放。

# 垃圾的定义

简单理解就是没有被引用的对象

# 垃圾回收的工作原理

应用程序根中遍历托管堆中的对象,标记哪些被使用对象(那些没人使用的就是所谓的垃圾),然后把可达对象转移到一个连续的地址空间(也叫压缩),其余的所有没用的对象内存被回收掉。

应用程序的根包含线程堆栈上的静态字段、局部变量、CPU 寄存器、GC 句柄和终结队列。

# 分代回收过程

主要分三步骤:标记、清除、压缩。

  • 标记:先假设所有对象都是垃圾,根据应用程序根 Root 遍历堆上的每一个引用对象,生成可达对象图,对于还在使用的对象(可达对象)进行标记(其实就是在对象同步索引块中开启一个标示位)
  • 清除:针对所有不可达对象进行清除操作,针对普通对象直接回收内存,而对于实现了终结器的对象(实现了析构函数的对象)需要单独回收处理。清除之后,内存就会变得不连续了,就是步骤 3 的工作了。
  • 压缩:把剩下的对象转移到一个连续的内存,因为这些对象地址变了,还需要把那些 Root 跟指针的地址修改为移动后的新地址。
# 分代 GC 算法

Net 框架为了使得垃圾回收更加高效,优化其性能,使用了分代垃圾回收算法,将托管堆分为 0/1/2 三代,至于为什么分代,是因为可以针对与每一代进行回收而不是每次将整个托管堆进行回收。另一个原因是在生产环境中的应用程序已经是优化好的,几乎所有的对象都在第 0 代中回收了。

当条件得到满足时,垃圾回收将在特定代上发生

  • 第 0 代:主要包含短生命周期对象,年轻代,当第 0 代托管堆已满时,再次创建对象 GC 会对第 0 代进行收集,回收之后未被回收的对象将提升到第 1 代中。
  • 第 1 代:主要包含第 0 代未回收的对象,它是第 0 代和第 2 代,也就是短生命周期和长生命周期之间的缓冲区。我们开发写的代码只能是分配在第 0、2 代,1 代作为 0 和 2 代之间的缓冲区,由 GC 处理。
  • 第 2 代:大对象堆和长生命周期的对象存在于此代中。当第 2 代垃圾回收完之后仍未回收的对象也会保留在第 2 代中。
# 暂时代和暂时段

第 0 代和第 1 代对象的生命周期较短,这两代称之为暂时代

暂时代在被称之为 “暂时段” 的内存段中进行分配

# 注意事项

回收一代时,同时也会回收它前面的所有代,所以说 第 2 代垃圾回收也称为完整垃圾回收,因为它回收所有代中的对象,如果第 0 代托管堆的回收没有回收足够的内存供应用程序创建新对象,垃圾回收器就会先执行第 1 代托管堆的回收,然后再执行第 2 代托管堆的回收。 第 1 级托管堆中未被回收的对象将会升级至第 2 级托管堆

# 垃圾回收优点
  • 开发人员不必关心内存的分配与释放,不必手动释放内存。
  • 能有效的分配托管堆上的对象。
  • 回收不再使用的对象,清除它们的内存,并保留内存以用于将来分配。 托管对象会自动获取干净的内容来开始,因此,它们的构造函数不必对每个数据字段进行初始化。
  • 通过确保对象不能自己使用分配给另一个对象的内存来提供内存安全
# Dispose 和 Finalize 方法

.NET 中提供释放非托管资源的方式主要是:Finalize () 和 Dispose (),如果不能及时释放非托管资源会造成内存泄漏,例如数据库连接不被释放就可能导致连接池中的可用数据库连接用尽。文件不关闭会导致其它进程无法读写这个文件等等。

Finalize虽然看似手动清除非托管资源,其实还是由垃圾回收器维护,它的最大作用是确保非托管资源一定被释放

  • 显示调用 Dispose 接口
  • using () 语法糖,其本质是 try...finally

垃圾回收器会在一下条件下自动调用 Finalize 方法

  • 垃圾回收器发现对象不可访问后,例外情况,在 Dispose 方法中调用了 GC.SuppressFinalize (object obj)(不要调用指定对象的终结器)
  • 仅在.NET Framework,在关闭应用程序域期间,除非对象不受最终化的影响。 在关闭期间,即使是仍可访问的对象也会最终完成
    Finalize 仅在给定实例上自动调用一次
# Dispose 与 Finalize 方法的不同点
  • Finalize 是 CLR 提供的一个机制,Dispose

  • Finalize 由垃圾回收器调用;Dispose 由对象调用仅仅是一个设计模式 (作为一个 IDisposable 接口的方法),可以及时手动调用非托管资源的释放,无需等到该类对象被垃圾回收那个时间点

  • Finalize 无需担心因为没有调用 Finalize 而使非托管资源得不到释放,而 Dispose 必须手动调用

  • Finalize 因为由垃圾回收器管理,不能保证立即释放非托管资源;而 Dispose 一调用便释放非托管资源

更新于