GC
GC: Garbage Collector
CLR: Common Language Runtime
- 在激活一个进程时,CLR会先保留一块连续的内存,在主线程启动过程中,可能会初始化一系列对象。CLR先计算对象大小以及其开销所占用的字节数,接着会在连续的内存块中为这些对象分配内存,这些内存被配置在第0代内存,在构造第0代内存的时候会分配一个默认大小的内存,随着程序的运行,可能会初始化更多的对象,CLR发现第0代内存不能装载更多的新生对象,此时CLR会启动垃圾回收器对第0代内存进行回收,不再使用的对象所占用的内存会被释放,接着把第0代对象提升为第1代,然后把新生对象配置在第0带内存中。
- CLR使用了3个阶段的代,每次新分配的对象都会被配置在第0代内存中,最老的对象再第2代内存中,每次为新对象分配内存时,都很可能会进行垃圾回收释放内存。很显然”CLR认为内存永远也使用不完。”
内存分配
垃圾回收是对引用类型而言的。
- 引用类型的对象从托管堆中分配内存,值类型从栈中分配内存(包括文件 数据库连接 套接字 COM对象等)。
- 在整个进程的生命周期中,CLR会维护一个指针P,一直指向当前进程所分配的最后一个对象内存的结尾处而不会跑出当前进程内存边界。
流程
线程挂起
将正在执行托管代码的所有线程挂起,挂起时,CLR会记录每个进程的指令指针以确定线程当前执行到哪里以便将来在回收结束后进行恢复。
如果一个线程的指令恰好达到一个安全点,则可以挂起该线程,否则CLR会尝试劫持该线程,如果还未到达安全点,则等待几百毫秒后CLR会尝试再一次劫持该线程,有可能经过多次尝试,最终挂起该进程。Mark-Sweep标记清楚阶段
假设heap中所有对象都可以回收,然后找出不能回收的对象,给这些对象打上标记,最后heap中没有打标记的对象都是可以回收的。标记对象 确定roots 垃圾回收期沿着线程栈检查所有的根,静态字段,方法参数,活动中的局部变量以及寄存器指向的对象都是根。代码中可能有多个地方引用同一个对象,垃圾回收器只要检测到对象已经标记过,则不再对对象内的所引用对象进行检测。
搬迁对象压缩堆 垃圾回收期遍历堆中所有对象来寻找未标记的对象,因为未标记的对象是垃圾对象,可以进行回收,如果发现对象过小可以忽略,否则先释放这些垃圾对象所占用的内存。再把可达对象搬迁到这里以压缩堆。
在搬迁可达对象后,所有指向这些对象的变量将无效,接着垃圾回收器要重新遍历引用程序的所有根来修改它们的引用。在这个过程中如果各个线程正在执行,很可能导致变量引用到无效的对象地址,所以整个过程的正在执行托管代码的线程是被挂起的。Compact压缩阶段
对象回收之后heap内存空间变得不连续,在heap中移动这些对象,使他们重新从heap基地址开始连续排列。类似于磁盘的碎片整理.
Heap内存经过回收压缩之后,可以继续采用前面的heap内存分配方法.
Generational 分代算法
将对象按照生命周期分为新的、老的。采用不同的回收策略和算法。
前提条件:
1. 大量新创建的对象生命周期都比较短,而较老的声明周期会更长
2. 对部分内存进行回收比对全部内存进行回收要快
3. 新创建的对象之间的关联性通常比较长,heap分配的对象是连续的,有利于提高CPU cache的命中率
Heap分为3个generation区域。(0,1,2)
* 如果Gen 0达到阈值,触发0代GC,幸存的进入Gen 1。
* 如果Gen 1达到阈值,触发1代GC,将Gen 0和Gen 1一起回收,幸存的进入Gen 2
* 如果Gen 2达到阈值,触发2代GC,将Gen 0,Gen 1,Gen 2一起回收
因此0代和1代GC的成本非常低。
大对象
在创建新对象时,任何大于等于85000字节的对象都被认为是大对象,这些对象的内存是从大对象堆中分配的,大对象总是被认为是第二代对象,要尽量避免分配大对象来减少性能损伤,为了提高性能,垃圾回收期不对大对象进行搬迁压缩,只在回收第二代内存时进行回收。
手工进行回收
每次内存回收过程都会导致性能损伤,尽量避免调用。
//对所有代进行回收
GC.Collect()
//对指定代进行回收
GC.Collect(int generation)
//强制在System.GCCollectionMode值所指定的时间对0~指定代进行垃圾回收
GC.Collect(int generation,GSCollectionMode mode)
GC缺点
- GC不能释放所有资源,不能释放非托管资源
- GC不是实时性的,可能造成性能上的瓶颈和不确定性
Dispoise
如果使用了非托管资源,或者需要显示释放的托管资源,就需要继承IDisposable.
当客户端记得的时候使用IDisposable接口释放你的非受控(unmanaged)资源,当客户端忘记的时候防护性地使用终结器(finalizer)。它与垃圾收集器一起工作,确保只在必要的时候该对象才受到与终结器相关的性能影响。
当垃圾收集器运行的时候,它立即从内存中删除所有不带终结器的垃圾对象。所有带有终结器的对象仍然存在于内存中。这些对象都被添加到终结队列,垃圾收集器引发一个新线程,周期性的在这些对象上运行终结器。在这些终结程序完成自己的工作后,就可以从内存中删除垃圾对象了。需要终结的对象再内存中停留的时间比没有终结器的对象停留的时间长很多。
public class SampleClass : IDisposable
{
//演示创建一个非托管资源
private IntPtr nativeResource = Marshal.AllocHGlobal(100);
//演示创建一个托管资源
private AnotherResource managedResource = new AnotherResource();
private bool disposed = false;
/// <summary>
/// 实现IDisposable中的Dispose方法
/// </summary>
public void Dispose()
{
//必须为true
Dispose(true);
//通知垃圾回收机制不再调用终结器(析构器)
GC.SuppressFinalize(this);
}
/// <summary>
/// 不是必要的,提供一个Close方法仅仅是为了更符合其他语言(如C++)的规范
/// </summary>
public void Close()
{
Dispose();
}
/// <summary>
/// 必须,以备程序员忘记了显式调用Dispose方法
/// </summary>
~SampleClass()
{
//必须为false
Dispose(false);
}
/// <summary>
/// 非密封类修饰用protected virtual
/// 密封类修饰用private
/// </summary>
/// <param name="disposing"></param>
protected virtual void Dispose(bool disposing)
{
if (disposed)
{
return;
}
if (disposing)
{
// 清理托管资源
if (managedResource != null)
{
managedResource.Dispose();
managedResource = null;
}
}
// 清理非托管资源
if (nativeResource != IntPtr.Zero)
{
Marshal.FreeHGlobal(nativeResource);
nativeResource = IntPtr.Zero;
}
//让类型知道自己已经被释放
disposed = true;
}
public void SamplePublicMethod()
{
if (disposed)
{
throw new ObjectDisposedException("SampleClass", "SampleClass is disposed");
}
//省略
}
}
Dispose & 析构
析构函数和Dispose都是释放资源,析构函数用于隐式释放资源,Dispose用于显示释放资源,也就是析构函数时对象不可访问后自动被调用的,而Dispose是类使用者调用的。
Dispose方法应释放所有托管和非托管资源,而析构函数只应释放非托管资源。因为析构函数由GC来判断调用,当GC判断某个对象不再需要的时候,则调用其析构方法,这时候该对象中可能还包含有其他有用的托管资源。
通过系统GC频繁的调用析构方法来释放资源会降低系统性能,所以推荐显示调用Dispose方法。
Dispose方法结尾处加上GC.SuppressFinalize(this)告诉GC不需要再调用该对象的析构方法,否则GC仍会在判断该对象不再有用后调用其析构方法,虽然程序不会出错,但是影响系统性能。
析构函数和Dispose释放的资源应该相同,这样即使在没有调用dispose的情况下,资源也会在Finalize中得到释放
析构不应为public
有Dispose方法存在时,应该调用它,因为Finalize释放资源通常是很慢的。