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