目前项目开发基本都基于.NetCore 3.1以上了,有些老版本的规则和概念也没有列出来,低版本的垃圾回收类型和内存释放方式会有所不同

垃圾回收器为什么存在
  • 开发人员不必手动释放内存。

  • 有效分配托管堆上的对象。

  • 回收不再使用的对象,清除它们的内存,并保留内存以用于将来分配。 托管对象会自动获取干净的内容来开始,因此,它们的构造函数不必对每个数据字段进行初始化。

  • 通过确保对象不能使用另一个对象的内容来提供内存安全。

托管堆代数

概述

为优化垃圾回收器的性能,将托管堆分为三代:第 0 代、第 1 代和第 2 代。目的是为了单独处理短生存期对象和长生存期对象。垃圾回收器大部分时间都在处理短生存期对象的回收。

底层一代的GC回收会触发年轻一代的GC回收,第二代的GC回收会触发完整的GC回收.

第0代(暂时代) 第1代(暂时代) 第2代 LOH(逻辑第3代)
所处内存段 暂时段 暂时段 非暂时段 非暂时段

LOH(大型对象堆)实际位于第二代
单独在第二代上为其划分了一块区域。
逻辑上称为第3代 |
| 包含 | 短生存期对象,即新分配的对象 | 短生存期对象,从第0代回收后,
未被回收的对象升级为第1代。 | 长生存期的对象,第一代回收后,
未被回收的对象升级为第2代。 | 对象的大小>= 85,000 字节 |
| 回收条件 | 第0代已分配内存达到阈值
如果第0代已满,仍尝试创建新对象
调用GC.Collect()方法
第1代GC回收 | 第1代已分配内存达到阈值
第0代回收之后仍然没有足够的空间存放新对象(此时会先回收第1代,再回收第2代)
调用GC.Collect方法
第2代GC回收 | 第2代已分配内存达到阈值
第0代回收之后仍然没有足够的空间存放新对象(此时会先回收第1代,再回收第2代)
调用GC.Collect方法
达到LOH回收条件
系统内存不足 | 达到第2代回收条件
大型对象内存分配达到阈值 |
| 回收方式 | 前台垃圾回收,当前托管线程被挂起 | 前台垃圾回收,当前托管线程被挂起 | 后台垃圾回收,当前托管线程正常执行 | 同第二代 |
| | | | | |

想要判断一个对象是否为大对象,可通过以下代码查看

var o = new Byte[85000];
Console.WriteLine(GC.GetGeneration(o));//GC2,大对象
o = new Byte[84900]; 
Console.WriteLine(GC.GetGeneration(o));  //GC0,小对象 84999仍是大对象,需要用一定量的内存空间保存指针 
var arr = new int[85000 / 4];	
Console.WriteLine(GC.GetGeneration(arr));//GC2,大对象,数组会提前开辟空间, int占32位,4个字节,85000 / 4加上指针内容会达到大对象的大小
arr = new int[85000 / 4 - 20];  
Console.WriteLine(GC.GetGeneration(arr));//GC0,小对象

阈值

当垃圾回收器检测到某个代中的幸存率很高时,它会增加该代的分配阈值,避免垃圾回收过于频繁地运行

但是阈值调大之后,会导致一次回收的内存过高。

所以阈值由CLR动态决定,以调节 回收频率单次回收内存大小的平衡

垃圾回收类型
工作站(默认方式) 服务器
特点 垃圾回收线程同用户线程优先级相同,会与用户线程争用CPU资源
只有一个处理器的计算机无论是否修改配置文件最终都会应用工作站垃圾回收方式 有垃圾回收的专用线程

线程优先级为THREAD_PRIORITY_HIGHEST 每个CPU都会分配一个垃圾回收专用线程和专用堆。不同的堆可以互通
多个垃圾回收线程一起工作,所以堆大小相同时,服务器垃圾回收比工作站垃圾回收快 |
| 适用场景 | 普通场景 | 需要高吞吐量和可伸缩性的服务器应用程序 |

内存释放

释放目标

GC释放应用程序不再使用的对象的内存,通过检查应用程序的根来确定不再使用的对象

应用程序的根包括:静态字段、局部变量、CPU 寄存器、GC 句柄和终结队列

释放步骤

- 列出不可访问对象和幸存对象的地址块并**标记**
- 使用内存复制功能压缩可以访问的对象到不可访问的地址块中,就是把存活下来的对象重新排列到连续的内存块中
- 大对象通常不会压缩,因为大对象所占用的内存区域过大,移动成本太大
- 回收死空间
- 指针更正,让对象指针指向新地址,指针更正是因为压缩了对象,对象在内存中的位置发生了变化

代码调优
  • 始终调用引用对象的Dispose方法,始终在实现了IDisposable的类中正确实现析构函数
  • 静态类中分配的对象不再使用后及时删除
  • 禁止在IOC声明为单例生命周期的类中注入瞬时生命周期的对象
  • 非必要时不要创建大型对象
  • 可视情况用ValueTask来代替Task,Task为引用类型,cpu密集型的调用会频繁触发第0代的GC回收
  • 尽可能重复使用HttpClient
  • 使用ArrayPoolMemoryPool从缓冲池中租用对象空间
  • 使用弱引用WeakReference重复使用已不再使用但尚未被回收的对象
监控及调试
  • 监听垃圾回收 ETW 事件,可用PerfView查看ETW事件,适用于window平台。也可在代码中引入Microsoft.Diagnostics.Tracing.TraceEventnuget包在代码中监听指定的GC回收等事件自定义后续处理逻辑

  • 使用性能监视器Perfmon.exe,适用于windows平台

  • 使用SOS调试,抓取dump转储文件后用WinDbg进行分析诊断,适用范围较广,可看到最全的内存信息

  • .Net CLI工具dotnet-counters,可以看到大概的性能指标数据统计结果,适用于临时运行状况查看和监视