C#-垃圾回收机制(GC)
GC是CLR的一个组件,它控制内存的分配和释放,它的出现是为了简化程序员的内存管理工作。
在面向对象的环境中,每个类型都可以代表可供程序使用的一种资源,访问资源的步骤:
上述的最后一步如果由程序员负责,可能会产生一些无法预测的问题(如:忘记释放不再使用的内存、试图使用已被释放的内存等等),因此GC被引入,单独负责这一步,简化了程序员的内存管理工作。
new
托管堆上有一个nextObjPtr指针,指向下一个对象在堆中分配的位置。
当应用程序执行new操作符后,若内存中有足够的可用空间,就在nextObjPtr处放入对象,接着调用对象的构造方法,并为应用程序返回一个该对象的引用。
nextObjPtr会加上当前对象占用的字节数,获得下一个对象放入托管堆时的地址。
GC即垃圾回收。它是以应用程序的root为基础,遍历应用程序在托管堆(Heap)上动态分配的所有对象,通过识别它们是否被引用来确定哪些对象是已经死亡的,哪些是仍需要被使用的。其中,已经不再被引用的对象就是已经死亡的对象,即所谓的垃圾,需要被回收。这就是GC工作的原理。
每个应用程序都包含一组root。每个root都是一个存储位置,其中包含指向引用类型对象的一个指针(可以理解为对象的引用)。该指针要么引用托管堆中的一个对象,要么为null。
在应用程序中,只要某对象变得不可达,也就是没有根(root)引用该对象,这个对象就会成为垃圾回收器的目标。
.NET中可以当作GC Root的对象有如下几种:
注意,只有引用类型的变量才被认为是root,值类型的变量永远不被认为是root。
进行一次完整内存区域的GC(full GC)操作成本很高,因此我们采用分代算法对GC性能进行一定改善。
分代算法的思想:将对象按照生命周期分成新老对象,对新、老区域采用不同的回收策略和算法,加强对新区域的回收处理力度,争取在较短时间间隔、较小的内存区域内,以较低成本将执行路径上大量新近抛弃不再使用的局部对象及时回收掉。
分代算法的假设前提条件:
.NET将heap分成3个代龄区域: Gen 0、Gen 1、Gen 2。
如果Gen 0 heap内存达到阀值,则触发0代GC,0代GC后Gen 0中幸存的对象进入Gen1。如果Gen 1的内存达到阀值,则进行1代GC,1代GC将Gen 0 heap和Gen 1 heap一起进行回收,幸存的对象进入Gen2。2代GC将Gen 0 heap、Gen 1 heap和Gen 2 heap一起回收。
Gen 0和Gen 1比较小,这两个代龄加起来总是保持在16M左右;Gen2的大小由应用程序确定,可能达到几G,因此0代和1代GC的成本非常低,2代GC称为fullGC,通常成本很高。
最常见的触发条件:CLR在检测第0代内存超过预算时触发一次GC。
代码显式调用GC.Collect()。
Windows报告低内存。
CLR正在卸载AppDomain。
CLR正在关闭。
在编写应用程序中肯定会涉及例如:操作文件 FileStream、网络资源socket、互斥锁 Mutex 等这些本机资源。
创建对象时不仅也要为它分配内存资源,还要为它分配本机资源。那么包含本机资源的类型被GC 时,GC会回收对象在托管堆中使用的内存,但这个类型的本机资源不清理的话,就会造成本机资源的泄漏。
所以,CLR提供了称为终结的机制,允许对象在被判定为垃圾之后,但在对象内存被回收之前执行一些代码。任何包装了本机资源的类型都支持终结。
CLR 判定一个对象不可达是,对象将终结它自己,释放它包装的本机资源。之后,GC 会从托管堆回收对象。
对于使用了本机资源的对象,在废弃它的时候我们该如何处理呢?
终极基类 System.Object 定义了受保护的虚方法 Finalize。如果你创建的对象使用了本机资源,你可以要重写Object 的虚方法。在类名前添加~ 符号来定义Finalize方法。垃圾回收器判定对象是垃圾后,会调用对象的Finalize 方法。
internal sealed class SomeType{
~SomeType()
{
//这里的代码会进入Finalize 方法
}
}
拥有本机资源的对象经历垃圾回收的顺序是这样的:
首先,栈是程序运行前就已经分配好的空间,所以运行时分配几乎不需要时间。而堆是运行时动态申请的,分配内存会有耗时。
其次,访问堆需要两次内存访问,第一次取得地址,第二次才是真正得数据,而栈只需访问一次。栈有专门的寄存器,压栈和出栈的指令效率很高,而堆需要由操作系统动态调度。
C# 程序在 .NET 上运行,而 .NET 是名为公共语言运行时 (CLR) 的虚执行系统和一组类库。CLR 是 Microsoft 对公共语言基础结构 (CLI) 国际标准的实现。 CLI 是创建执行和开发环境的基础,语言和库可以在其中无缝地协同工作。
用 C# 编写的源代码被编译成符合 CLI 规范的中间语言(IL)。 IL 代码和资源(如位图和字符串)存储在扩展名通常为 .dll 的程序集中。
执行 C# 程序时,程序集将加载到 CLR。 CLR 会直接执行实时 (JIT) 编译,将 IL 代码转换成本机指令。 CLR 可提供其他与自动垃圾回收、异常处理和资源管理相关的服务。 CLR 执行的代码有时称为“托管代码”。而“非托管代码”被编译成面向特定平台的本机语言。
内存泄漏指的是程序中不再需要的内存没有被释放,从而导致内存使用不断增加,最终可能导致系统性能下降或应用程序崩溃。
C#的内存泄露情况有以下几种:
1.委托或事件没有解除注册。
2.静态引用:如果一个静态对象长时间存活且占用大量内存,并且该对象不会被释放或重置,可能导致内存泄漏。
3.长生命对象:如果对象的生命周期很长,而它又引用了大量短命对象,这些短命对象就无法被回收,从而导致内存泄漏。
4.未释放非托管资源:尽管垃圾回收器可以自动管理托管内存,但对非托管理资源(如文件句柄、数据库连接等)仍然需要手动释放。如果未正确释放这些资源,会导致内存泄漏(可以用接口IDispose进行释放)。
弱引用(Weak Reference)是一种特殊的引用类型,它允许你引用一个对象而不阻止该对象被垃圾回收器(GC)回收。换句话说,弱引用不会延长对象的生命周期。
var strongReference = new object(); // 创建一个强引用对象
var weakReference = new WeakReference<object>(strongReference); // 创建一个对该对象的弱引用
strongReference = null; // 删除强引用
int[] arrayA = new int[n];
int[] arrayB = new int[]{
1,2,3};
//两行三列的二维数组
int[,] arrayA = new int[2,3];
int[,] arrayB = new int[,]{
{
1,2,3},
{
3,2,1},
};
交错数组可以理解为数组的数组。
//由于交错数组存放的数组长度可能各不相同,所以不指定第二维度
int[][] arrayA = new int[2][];
int[][] arrayB