CLR和.Net对象生存周期
.NET Framework和.NET Core为开发者提供了一个强大的应用程序执行环境,称为公共语言运行时(CLR)。CLR是一个由Microsoft开发的、提供.NET应用程序运行时功能的组件。它处理了许多底层任务,包括内存管理、程序集加载、异常处理、垃圾回收以及线程同步等。CLR的目的是让开发者能够专注于编写应用程序逻辑,而不用担心许多与平台相关的细节。
什么是CLR:
-
CLR(Common Language Runtime)是.NET Framework的核心组件之一。它是一个管理和执行.NET应用程序的运行时环境,负责处理代码的执行和监控。
-
CLR并不关心开发人员使用什么语言来进行编程,因为它主要关注的是执行中间语言(IL)代码。开发人员可以使用不同的语言编写.NET应用程序,只要它们的编译器能够生成面向CLR的中间语言(IL)代码。最终,这些编译器会生成包含中间代码(IL)和托管数据的托管模块,这使得.NET应用程序能够在CLR上运行。
-
CLR的核心功能包括内存管理,程序集加载,类型安全,异常处理和线程同步,而且还负责对代码实施严格的类型安全检查,保证代码的准确性,这些功能都可以提供给面向CLR的所有语言(C#,F#等)使用。
CLR的内存管理功能包括自动垃圾回收,这意味着开发人员无需手动释放内存。程序集加载方面,CLR负责动态加载和链接程序集,并在需要时执行。与此同时,CLR负责严格的类型安全检查,这有助于避免常见的内存错误和安全漏洞。异常处理是另一个关键功能,它确保在代码执行过程中的异常情况得到正确处理。此外,CLR还管理线程同步,以确保在多线程环境中的正确执行。
-
什么是托管代码和非托管代码
-
托管代码:托管代码是由CLR环境执行的代码,它能够利用CLR提供的服务,如自动垃圾回收、运行库类型检查和安全支持等。这使得托管代码应用程序能够表现出统一、独立于平台和语言的行为。相比之下,非托管代码是在CLR环境之外,由操作系统直接执行的代码。
-
非托管代码:非托管代码需要自行提供垃圾回收、类型检查和安全支持等服务。典型的非托管代码包括COM/COM++组件、ActiveX控件和API函数。通常情况下,非托管代码需要手动进行资源回收,例如调用Dispose接口或使用using语句来包裹相关逻辑块,确保资源得到正确释放。
当与非托管代码交互时,托管代码需要手动管理内存,以下是一个扩展示例:
非托管代码示例(C++):
// unmanaged.dll
#include <iostream>
extern "C" __declspec(dllexport) int Add(int a, int b)
{
return a + b;
}
extern "C" __declspec(dllexport) void* AllocateMemory(int size)
{
return new char[size];
}
extern "C" __declspec(dllexport) void FreeMemory(void* pointer)
{
delete[] pointer;
}
托管代码示例(C#):
using System;
using System.Runtime.InteropServices;
class Program
{
[DllImport("unmanaged.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int Add(int a, int b);
[DllImport("unmanaged.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr AllocateMemory(int size);
[DllImport("unmanaged.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void FreeMemory(IntPtr pointer);
static void Main()
{
int result = Add(5, 3);
Console.WriteLine("The result is: " + result);
// 手动分配和释放内存
IntPtr memory = AllocateMemory(100); // 分配内存
// 使用内存
// ...
// 释放内存
FreeMemory(memory);
}
}
在这个扩展示例中,除了调用非托管DLL中的Add函数外,我们还添加了两个额外的函数AllocateMemory和FreeMemory,用于手动分配和释放内存。这些函数允许C#托管代码通过调用非托管代码来分配和释放内存。
CLR要求所有对象(主要指引用类型)都用new操作符创建,new操作符在完成四步操作以后,会返回指向托管堆上新建对象的一个引用(或指针,视情况而定),在使用完以后,C#并没有如C++对应的delete操作符来删除对象,也就是说,开发人员是没有办法显示释放为对象分配的内存,但是CLR采用了垃圾回收机制,能够自动检测到一个对象是否可达,并且自动释放资源。
//四步操作
1.它计算类型以及所有基类型(一直到System.Object,虽然它没有定义自己的实例字段)中定义的所有实例字段需要的字节数。堆上的每个对象都需要一些额外的成员---即“类型对象指针”和“同步块索引”。 这些成员由CLR用于管理对象。这些额外成员的字节数会计入对象大小。
2.它从托管堆中分配制定类型要求的字节数,从而分配对象的内存,分配的所有字节都设为0.
3.它初始化对象的“类型对象指针”和“同步块索引”成员。
4.调用类型的实力构造器,向其传入在对new的调用中指定的任何实参。大多数编译器都在构造器重自动生成代码来调用一个基类构造。每个类型的构造在调用时,都要负责初始化由这个类型定义的实例字段。最终调用的说System.Object的构造器,该构造器知识简单地返回,不会做其他任何事情。为了证明这一点,可使用ILDasm.exe加载MSCorLib.dll,检查System.Object的构造器。
垃圾回收机制
垃圾回收器(GC)是CLR中的一个核心组件,它自动释放不再使用的内存。GC主要基于对象的可达性来判断对象是否仍然有存在的必要。如果从应用程序的根(全局变量、静态变量等)无法到达某个对象,这个对象就被认定为垃圾并且会在下一次垃圾回收过程中被清除。
-
垃圾回收器(Garbage Collector)简称GC,采用引用跟踪算法,在CLR中用作自动内存管理器,用于控制的分配和释放的托管内存。
-
开发应用程序时不必释放内存。
-
有效分配托管堆上的对象。
-
回收不再使用的对象,清除它们的内存,并保留内存以用于将来分配。托管对象会自动获取干净的内容来开始,因此,它们的构造函数不必对每个数据字段进行初始化。
-
通过确保对象不能使用另一个对象的内容来提供内存安全。
大多数类型并不需要我们进行资源清理,GC会自动释放内存。只是针对于一些特殊对象时,如文件占用,数据库连接,开发人员才需要手动销毁资源占用空间。
为什么需要垃圾回收
- 运行环境内存总是有限的
什么时候进行垃圾回收
当满足以下条件之一时CLR将发生垃圾回收:
- 系统具有低的物理内存。
- 由托管堆上已分配的对象使用的内存超出了可接受的阈值(即将涉及到代的概念)。随着进程的运行,此阈值会不断地进行调整。
- 强制调用 GC.Collect 方法。
- CLR正在卸载应用程序域(AppDomain)
- CLR正在关闭。
GC运作的时候会经历几个阶段:
- 暂停阶段:暂停应用中的所有线程以防止在垃圾回收过程中发生状态改变。
- 标记阶段:遍历所有根对象,标记所有可达的对象。
- 碎片整理阶段:清除未被标记的对象,并整理剩余对象使其在内存中连续排列,这有助于提高内存分配的效率。
- 恢复阶段:恢复应用中被暂停的线程。
垃圾回收时发生了什么
具体流程如下:
- GC的准备阶段
在这个阶段,CLR会暂停进程中的所有线程,这是为了防止线程在CLR检查根期间访问堆。 - GC的标记阶段
当GC开始运行时,它会假设托管堆上的所有对象都是垃圾。也就是说,假定没有根对象,也没有根对象引用的对象,然后GC开始遍历根对象并构建一个由所有和根对象之间有引用关系对象构成的对象图,然后,GC会挨个遍历根对象和引用对象,假如一个根包含null,GC会忽略这个根并继续检查下个根(这很关键)。反之,假如根引用了堆上的对象,GC就会标记那个对象并加入对象图中。如果GC发现一个对象已经在图中就会换一个路径继续遍历。这样做有两个目的:一是提高性能,二是避免无限循环。
- GC的碎片整理阶段
所有的根对象都检查完之后,GC构建的对象图中就有了应用程序中所有的可达对象。托管堆上所有不在这个图上的对象就是要做回收的垃圾对象了。同时,CLR会对堆中非垃圾对象进行位置上的整理,使它们覆盖占用连续的内存空间(这个动作还伴随着对根返回新的内存地址的行为),这样一方面恢复了引用的“局部化”,压缩了工作集,同时空出了空间给其他对象入住,另外也解决了本机堆的空间碎片化问题。 - GC恢复阶段
完成了综上的所有操作后,CLR也恢复了原先暂停的所有线程,使这些线程可以继续访问对象。
可以看到不可达的BEGIJ对象都已经被回收了,并且可达对象的位置也重新排列了,NextObjPtr依然指向最后一个可达对象之后的位置,为CLR下一次操作对象标识分配位置。
通过以上描述可知,不同于C/C++需要手动管理内存,GC的自动垃圾回收机制为我们解决了可能存在的内存泄漏和因为访问被释放内存而造成的内存损坏的问题。
代数的概念(Generation)
垃圾回收,与流程描述相似,会导致显著性能损失,这是使用托管堆的一个明显缺点。CLR的垃圾回收器是基于代的分代式垃圾回收器,代是为了降低GC对性能影响而设计的机制,其思路很简单:
- 对象越新,生命周期越短,反之亦然
- 回收托管堆的一部分,速度快于回收整个堆
每个对象在托管堆中都分为0、1、2三个代(System.GC.MaxGeneration=2):
- 第 0 代: 从未被标记为回收的新分配对象
- 第 1 代: 在上一次垃圾回收中未被回收的对象
- 第 2 代: 经过一次以上的垃圾回收后仍未被回收的对象
让我们通过图示具体了解代的工作原理吧
- 程序初始化时,托管堆不包含对象,此时添加到堆的对象为第 0 代对象,这些对象未经过GC检查。一段时间后,C,F,H对象被标记为不可达。
- CLR初始化时为第 0 代对象选择一个预算容量。如A-H对象正好用完了第 0 代的空间,此时再操作时就会引发一次GC操作。GC后第 0 代对象不包括任何对象,并且第 1 代对象也已经被压缩整理到连续的地址空间中。
Tips:垃圾回收发生于第 0 代满的时候
- 每次新对象仍然会被分配到第 0 代中。如图所示,CLR又重新分配了I-N对象,一段时间后,第 0 代和第 1 代都产生了新的垃圾对象
Tips:CLR不仅为第 0 代对象选择了预算,也为第 1 代,第 2 代对象选择了预算。
由于GC是自调节的,这意味着GC可能会根据应用程序构造对象的实际情况调整每代的预算(每次GC后,发现对象多存活增加预算,发现少存活减少预算),这样进程工作集大小也会实时不同,进一步优化了GC性能。
- 当CLR再为第 0 代对象加入新对象时造成超过第 0 代预算的情况,GC将重新开启。GC将检查第 1 代预算使用情况,假如第 1 代占用内存远少于预算,GC将只检查第 0 代对象,即便此时原来的第 1 代对象中也出现了垃圾对象。这符合假设中的第一点,同时GC也不用再遍历整个托管堆,从而优化了GC操作性能。
- 此后,CLR仍按规则对第 0 代分配对象,直到第 0 代预算被塞满才会发生垃圾回收,将对象补充到第 1 代中。此时分两种情况:如果第 1 代对象空间仍小于预算,第 1 代中的垃圾对象不会进行回收(如图4所示)。如果第 1 代对象在某个时间段增长到超过预算的阶段,CLR将在下一次进行GC回收时,检查第 1 代对象,然后统一回收第 0 代和第 1 代中的垃圾对象。回收后,第 0 代的幸存对象提升到第 1 代,第 1 代的幸存对象提升到第 2 代。此时第 0 代回归空余状态
- 至此,CLR已进行数次GC操作才最终将对象分配到了第 2 代中
非托管对象资源回收
尽管CLR可以自动管理内存,但对于非托管资源(如文件句柄、网络连接等)的清理则需要开发者手动处理。为了更好地管理这些资源,.NET提供了两种机制:
可终结对象(Finalize) ---------------类似于析构函数(但是执行时机不确定,主要取决于GC操作什么时候发生)
- 当包含本机资源的类型被GC时,GC会回收对象在托管堆上使用的内存,同时提供了一种称为终结器(Finalization)的机制,允许对象在判定为垃圾之后,在对象内存在回收之前执行一些代码。当一个对象被判定不可达后,对象将终结它自己,并释放包装着的本机资源,之后,GC再从托管堆中回收对象。
如果你必须要使用Finalize的话,给出的建议是“确保Finalize方法尽可能快的执行,要避免所有可能引起阻塞的操作,包括任何线程同步操作,同时也要确保Finalize方法不会引起任何异常,如果有异常垃圾回收器会继续执行其他对象的Finalize方法直接忽略掉异常”。
可处置对象(IDisposable)
上文提到Finalize的一些不可避免的缺点,特别是Finalize方法的执行时间是无法控制的,所以假如开发人员想要尽可能快地手动清除本机资源时,可以实现IDisposable接口, 它定义了一个名为Dispose()的方法。这也是我们熟悉的开发模式,比如FileStream类型便实现了IDisposable接口,所以具体的使用这里便不再赘述。只是需要额外说明的是,并不一定要显式调用Dispose方法,才能保证非托管资源得到清理,调用Dispose方法只是控制这个清理动作的发生时间而已。同样的,Dispose方法也不会将托管对象从托管堆中删除,我们要记住在正常情况下,只有在GC之后,托管堆中的内存才能得以释放。我们的习惯用法是将Dispose方法放入try finally的finally块中,以确保代码的顺利执行。
总结
理解和正确使用CLR的内存管理功能对于编写高效、可靠的.NET应用程序至关重要。通过托管代码的自动内存管理和对非托管资源的显式清理,开发者可以在.NET平台上构建高性能的应用程序,同时避免了传统编程语言中常见的内存泄漏和资源泄露问题。
标签:对象,托管,回收,Unity,GC,内存,CLR From: https://www.cnblogs.com/jobshenlei/p/18137053