写在前面:在.NET程序开发中,为了将开发人员从繁琐的内存管理中解脱出来,将更多的精力花费在业务逻辑上,CLR提供了自动执行垃圾回收的机制来进行内存管理,开发人员甚至感觉不到这一过程的存在。.NET程序可以找出某个时间点上哪些已分配的内存空间没有被程序使用,并自动释放它们。自动找出并释放不再使用的内存空间机制,就称为垃圾回收机制。本文主要介绍.Net中的GC(垃圾回收)机制及其整体流程。
本文关键字:CLR、.Net、GC(垃圾回收)、C#、面试
一、定义
CLR执行垃圾回收的过程,有以下几点:
- 如何判断哪些对象是可以进行回收的,哪些是要保留的?
- 对象在堆上是如何分布的?何时执行垃圾回收?
- 垃圾回收的过程如何进行的?有哪些优化策略?
1. 什么是GC
.NET程序可以找出某个时间点上哪些已分配的内存空间没有被程序使用,并自动释放它们。
自动找出并释放不再使用的内存空间机制,就称为垃圾回收机制(Garbage Collection,简称GC)。
2. 栈空间和堆空间
为数据申请内存空间的操作称为分配,释放与申请内存空间的操作被称为释放。
每个线程都有独立的栈空间,栈空间用于保存调用函数的数据,堆空间是程序中一块独立的空间,从堆空间分配的数据可以被程序中的所有函数和线程访问,并且不会随函数返回与线程结束释放。
3.值类型和引用类型
值类型的对象本身存储值,而引用类型的对象本身存储内存地址,值存储在内存地址指向的空间中。
值类型与引用类型的对象本身存储在栈空间还是堆空间是根据定义的位置而定的。
值类型的对象会根据定义的位置隐式分配与释放。
引用类型的对象需要通过new关键字显示分配,new会从堆空间申请一块空间用于保存值,然后返回空间的开始地址。
二、.NET 中的 GC
垃圾回收机制的主要工作是找出堆空间分配的空间中哪些空间不再被程序使用,然后回收这些空间。在.NET中使用的方式,是最主流的"标记并清除"的方式。.NET中的根对象,包括各个线程栈空间上的变量,全局变量、GC句柄和析构队列中的对象。
1. 分代
.NET中将引用类型的对象分为三类,分别是第0代、第1代与第2代。
第0代中的对象存活时间通常最短,第1代中的对象存活时间比较长,第2代中的对象存活时间最长。
分代依据的目的是,尽量增加每次执行垃圾回收处理时,可回收的对象的数量,并减少处理所需的时间。
2. 压缩
反复执行分配与回收操作,可能导致堆上产生很多空余空间,这些空余空间又被称为碎片空间。
压缩机制可以通过移动已分配空间把碎片空间合并到一块,使得堆可以分配更大的对象。
.NET运行时提供的GC是支持压缩机制的,但是只在一定的条件下启用。
3. 大小对象
.NET根据引用类型对象值占用的空间大小来区分是小对象还是大对象。
大对象与小对象会在不同的堆区域中分配:大对象堆和小对象堆。
移动大对象需要的成本很高,前面我们提到的压缩机制,默认只在小对象堆启用,大对象堆是不会执行压缩的。
4. 固定对象
托管代码传递引用类型对象给非托管代码时必须创建固定类型的GC句柄,并在托管代码中保持这个句柄存活到非托管代码的调用结束。
创建了固定类型GC句柄的对象就称为固定对象。
使用固定对象会带来一些副作用,那就是由固定对象带来的碎片空间是无法合并的。
5. 析构队列
如果在垃圾回收的过程中执行这些析构函数,垃圾回收需要的时间是不可预料的。
如果对象不再存活但定义了析构函数,那么对象会添加到析构队列并标记存活。
析构函数执行完毕的对象,可以在下一轮GC中被回收。
析构函数如下:
public class Class : IDisposable
{
public int ClassId { get; set; }
public string ClassName { get; set; }
~Class()
{
MyLog.Log($"执行{this.GetType().Name}Dispose");
}
public void Dispose()
{
MyLog.Log($"执行{this.GetType().Name}Dispose");
}
}
6.STW
对象之间的引用关系会随着程序运行不断改变,让执行GC的线程与执行其他处理的线程同时运行会带来一些问题。
让执行GC处理以外的线程全都暂停运行,像这样的停止操作我们称为STW(Stop The World)。
三、工作站模式与服务器模式
工作站模式,适用于内存占用量小的程序和桌面程序,它可以提供更短的响应时间。
服务器模式适用于内存占用量大的程序与服务程序,可以提供更高的吞吐量。
四、普通GC与后台GC
普通GC会导致更长的单次STW停顿时间,但消耗的资源比较小,并且支持压缩处理。
后台GC每次STW停顿时间会更短,但停顿次数与消耗的资源会更多,并且不支持压缩处理。
五、引用类型的数据结构
引用类型对象的值由三个部分组成,分别是对象头、类型信息和各个字段的内容。
1. 对象头
对象头包含了标志与同步块索引等数据。
高1位用于.NET运行中内部检查托管堆状态时,标记对象是否已检查。
高2位用于标记是否抑制运行对象的析构函数。
高3位用于标记对象是否为固定对象。
高4、5、6为用于标记低26位保存了什么内容,其中就包括了获取锁、释放锁和对象Hash值的信息。
2. 类型信息
类型信息是一个指向的是.NET运行时内部保存的类型数据(MethodTable)的内存地址。
类型数据包含了类型的所属模块名称、字段列表、属性列表、方法列表,以及各个方法的入口点的地址等信息。
六、.NET 程序的内存结构
1. 托管堆和堆段
托管堆用于保存引用类型对象的值;
每个堆段默认的大小同样根据GC模式与运行环境的CPU逻辑核心数量来决定;
GC模式 | CPU逻辑核心数 | 32位 | 64位 |
---|---|---|---|
工作站模式 | —— | 16MB | 256MB |
服务器模式 | <4 | 64MB | 4GB |
服务器模式 | >=4&&<8 | 32MB | 2GB |
服务器模式 | >=8 | 16MB | 1GB |
2. 分配上下文
在.NET中,托管堆每个区域的小对象堆有三个代,大对象堆有一个代(第2代),这些代会通过generation类型的实例进行管理。
代的开始地址决定了哪些对象在哪些代?
3. 自由对象列表
如果自由空间出现在以分配空间的尾部,那么它会释放给操作系统,并且所占空间会归为未分配空间。
堆段上自由对象所占的空间可以称为碎片空间。
托管堆的每个区域有4个自由对象列表,它们分别记录,第0代的自由对象、第1代的自由对象、第2代小对象堆段的自由对象、第2代大对象堆段的自由对象。
为了提升从用自由对象的效率,第2代的自由对象列表,还会根据自由对象的大小进行分组。
4. 跨代引用记录
.NET 实现分代的主要原因是为了支持垃圾回收时只处理一部分对象。
.NET 中有一个数组专门记录跨代引用,这个数组又称为卡片表,卡片表会标记所有发生夸代引用的位置。
卡片束记录卡片表中哪些位置有标记,先扫描卡片束再扫描卡片表就可以减少处理时间。
5. 析构对象与析构队列
七、GC的总体流程
1. GC的触发
第1个条件是 分配对象时找不到可用空间。第2个条件是分配量超过阈值。
第3个条件是托管代码主动调用GC.Cllect函数。
第4个条件是收到物理内存不足的通知。
2. 分配对象时找不到可用空间
第1种是针对第1代的GC,这一种GC会尝试回收短暂堆段上的对象,使得短暂堆段有更多空间。
第2种是针对第2代的GC,也就是完整GC,这种GC会在物理内存不足或执行第1种GC以后仍然无法分配时触发。
3. 分配量超过阈值
如果在某个代分配的对象值大小合计超过分配量域值,就会触发针对这个代的GC。
存活下来的对象越多,新分配量阈值越高。
4. GC.Collect
托管代码中调用GC.Collect函数可以主动触发GC,这个函数最多可以接收4个参数。
public static void Collect(
int generation,
GCCollectionMode mode,
bool blocking,
bool compacting
);
5. 物理内存不足
物理内存接近用尽时,操作系统会把物理内存中的部分内容移动到分页文件。
.NET为了避免因使用分页文件带来的性能低下,会自动检测物理内存是否接近不足,然后触发GC。
写在结尾:文章中出现的任何错误请大家批评指出,一定及时修改。
希望看到这里的小伙伴能给个三连支持!