【CLR C#】浅谈.Net的GC(垃圾回收)机制及其整体流程

简介: 在.NET程序开发中,为了将开发人员从繁琐的内存管理中解脱出来,将更多的精力花费在业务逻辑上,CLR提供了自动执行垃圾回收的机制来进行内存管理,开发人员甚至感觉不到这一过程的存在。.NET程序可以找出某个时间点上哪些已分配的内存空间没有被程序使用,并自动释放它们。自动找出并释放不再使用的内存空间机制,就称为垃圾回收机制。本文主要介绍.Net中的GC(垃圾回收)机制及其整体流程。
写在前面:

在.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句柄和析构队列中的对象。

标记.png

1. 分代

.NET中将引用类型的对象分为三类,分别是第0代、第1代与第2代。

第0代中的对象存活时间通常最短,第1代中的对象存活时间比较长,第2代中的对象存活时间最长。

分代依据的目的是,尽量增加每次执行垃圾回收处理时,可回收的对象的数量,并减少处理所需的时间

分代算法.png

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的线程与执行其他处理的线程同时运行会带来一些问题。

STW.png

让执行GC处理以外的线程全都暂停运行,像这样的停止操作我们称为STW(Stop The World)。

三、工作站模式与服务器模式

工作站模式,适用于内存占用量小的程序和桌面程序,它可以提供更短的响应时间。
服务器模式适用于内存占用量大的程序与服务程序,可以提供更高的吞吐量。

工作站模式与服务器模式.png

四、普通GC与后台GC

普通GC会导致更长的单次STW停顿时间,但消耗的资源比较小,并且支持压缩处理。
后台GC每次STW停顿时间会更短,但停顿次数与消耗的资源会更多,并且不支持压缩处理。

普通GC与后台GC.png

五、引用类型的数据结构

引用类型对象的值由三个部分组成,分别是对象头类型信息各个字段的内容

1. 对象头

对象头包含了标志与同步块索引等数据。

高1位用于.NET运行中内部检查托管堆状态时,标记对象是否已检查。

高2位用于标记是否抑制运行对象的析构函数。

高3位用于标记对象是否为固定对象。

高4、5、6为用于标记低26位保存了什么内容,其中就包括了获取锁、释放锁和对象Hash值的信息。

2. 类型信息

类型信息是一个指向的是.NET运行时内部保存的类型数据(MethodTable)的内存地址。

类型数据包含了类型的所属模块名称、字段列表、属性列表、方法列表,以及各个方法的入口点的地址等信息。

六、.NET 程序的内存结构

程序的内存结构.png

1. 托管堆和堆段

托管堆用于保存引用类型对象的值;

托管堆和堆段.png

每个堆段默认的大小同样根据GC模式与运行环境的CPU逻辑核心数量来决定

GC模式 CPU逻辑核心数 32位 64位
工作站模式 —— 16MB 256MB
服务器模式 <4 64MB 4GB
服务器模式 >=4&&<8 32MB 2GB
服务器模式 >=8 16MB 1GB

2. 分配上下文

分配上下文1.png

分配上下文2.png

在.NET中,托管堆每个区域的小对象堆有三个代,大对象堆有一个代(第2代),这些代会通过generation类型的实例进行管理。
代的开始地址决定了哪些对象在哪些代?

分代的实现.png

3. 自由对象列表

自由对象列表1.png

如果自由空间出现在以分配空间的尾部,那么它会释放给操作系统,并且所占空间会归为未分配空间。

自由对象列表2.png

堆段上自由对象所占的空间可以称为碎片空间。
托管堆的每个区域有4个自由对象列表,它们分别记录,第0代的自由对象、第1代的自由对象、第2代小对象堆段的自由对象、第2代大对象堆段的自由对象。
为了提升从用自由对象的效率,第2代的自由对象列表,还会根据自由对象的大小进行分组。

4. 跨代引用记录

.NET 实现分代的主要原因是为了支持垃圾回收时只处理一部分对象。

.NET 中有一个数组专门记录跨代引用,这个数组又称为卡片表,卡片表会标记所有发生夸代引用的位置。

卡片束记录卡片表中哪些位置有标记,先扫描卡片束再扫描卡片表就可以减少处理时间。

5. 析构对象与析构队列

析构对象与析构队列.png

七、GC的总体流程

1. GC的触发

流程.png

第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。


写在结尾:

文章中出现的任何错误请大家批评指出,一定及时修改。

希望看到这里的小伙伴能给个三连支持!

相关文章
|
11月前
|
存储 算法 Java
G1原理—5.G1垃圾回收过程之Mixed GC
本文介绍了G1的Mixed GC垃圾回收过程,包括并发标记算法详解、三色标记法如何解决错标漏标问题、SATB如何解决错标漏标问题、Mixed GC的过程、选择CollectSet的算法
G1原理—5.G1垃圾回收过程之Mixed GC
|
11月前
|
存储 算法 Java
G1原理—6.G1垃圾回收过程之Full GC
本文详细探讨了G1垃圾回收器对Full GC(FGC)的优化处理,涵盖FGC的前置处理、整体流程及并行化改进。重点分析了传统FGC串行化的局限性以及G1通过Region分区和RSet机制实现并行标记的优势,包括任务窃取提升效率、跨分区压缩以生成空闲Region等技术细节。此外,文章还介绍了G1的新特性——字符串去重优化,通过判断char数组一致性减少重复字符串占用内存,从而提升内存使用效率。总结部分全面回顾了G1在FGC中的各项优化措施及其带来的性能改善。
G1原理—6.G1垃圾回收过程之Full GC
|
11月前
|
存储 算法 Java
G1原理—4.G1垃圾回收的过程之Young GC
本文详细解析了G1垃圾回收器中YGC(Young Generation Collection)的完整流程,包括并行与串行处理阶段。内容涵盖YGC相关参数设置、YGC与Mixed GC及FGC的关系、新生代垃圾回收的具体步骤(如标记存活对象、复制到Survivor区、动态调整Region数量等),以及并行阶段的多线程操作和串行阶段的关键任务(如处理软引用、整理卡表、重构RSet)。
G1原理—4.G1垃圾回收的过程之Young GC
|
算法 网络协议 Java
【JVM】——GC垃圾回收机制(图解通俗易懂)
GC垃圾回收,标识出垃圾(计数机制、可达性分析)内存释放机制(标记清除、复制算法、标记整理、分代回收)
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
980 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
监控 算法 Java
深入理解Java中的垃圾回收机制(GC)
本文将探讨Java的自动内存管理核心——垃圾回收机制。通过详细解析标记-清除算法、复制算法和标记-整理算法等常用垃圾回收算法,以及CMS、G1等常见垃圾回收器,帮助读者更好地理解Java应用的性能优化和内存管理。同时,探讨分代收集、分区收集等策略在实际项目中的应用。结语部分总结了垃圾回收机制在Java开发中的重要性,并展望了未来可能的发展。
458 27
|
监控 算法 Java
深入理解Java中的垃圾回收机制在Java编程中,垃圾回收(Garbage Collection, GC)是一个核心概念,它自动管理内存,帮助开发者避免内存泄漏和溢出问题。本文将探讨Java中的垃圾回收机制,包括其基本原理、不同类型的垃圾收集器以及如何调优垃圾回收性能。通过深入浅出的方式,让读者对Java的垃圾回收有一个全面的认识。
本文详细介绍了Java中的垃圾回收机制,从基本原理到不同类型垃圾收集器的工作原理,再到实际调优策略。通过通俗易懂的语言和条理清晰的解释,帮助读者更好地理解和应用Java的垃圾回收技术,从而编写出更高效、稳定的Java应用程序。
|
缓存 监控 Java
"Java垃圾回收太耗时?阿里HBase GC优化秘籍大公开,让你的应用性能飙升90%!"
【8月更文挑战第17天】阿里巴巴在HBase实践中成功将Java垃圾回收(GC)时间降低90%。通过选用G1垃圾回收器、精细调整JVM参数(如设置堆大小、目标停顿时间等)、优化代码减少内存分配(如使用对象池和缓存),并利用监控工具分析GC行为,有效缓解了高并发大数据场景下的性能瓶颈,极大提升了系统运行效率。
474 4
|
存储 Java PHP
【JVM】垃圾回收机制(GC)之引用计数和可达性分析
【JVM】垃圾回收机制(GC)之引用计数和可达性分析
254 0
|
缓存 Java Python
python垃圾回收&缓存机制
python垃圾回收&缓存机制