分享C# GC 非原创

一. 托管资源的分配数组

CLR在运行时管理着一段内存地址空间(虚拟地址空间,在运行中会映射到物理内存地址中),分为“托管堆”和“栈”两部分,栈用于存储值类型数据,它会在方法执行结束后自动销毁其中引用的值类型变量,这一部分不属于垃圾收集的范围。托管堆用于引用类型的变量存储,是垃圾收集的关键阵地。安全

托管堆是一段连续的地址空间,其中所分配出去的空间呈现出相似数组形态的队列结构:多线程

 

NextObjPtr是托管堆所维护的一个内存指针,指示下一个对象分配的内存起始地址,它会随着内存的分配而不断移动(固然也会随着内存垃圾回收而发生移动),永远指向下一个空闲的地址。框架

到了这里,咱们不妨与C++比较一下内存分配机制的效率(对效率不感兴趣的大能够跳过:)),顺便让C++的朋友们打消一些对CLR分配内存效率的疑虑。在查找空闲内存空间时,CLR只须要在NextObjPtr处直接留出指定大小的空间提供给数据初始化,而后计算新的空闲地址并重置NextObjPtr指针便可。而在C/C++中,在分配内存以前先要遍历一遍内存占用的链表以查找合适大小的内存块,而后再修改此链表,这样也很容易产生内存碎块,使得内存分配性能降低。很明显,.NET的分配方式效率更高。可是这种效率是以GC的劳动为代价的。ide

二. 垃圾断定函数

要进行垃圾收集,首先要知道什么是垃圾。GC经过遍历应用程序中的“根”来寻找垃圾。咱们能够认为根是一个指向引用类型对象内存地址的指针。若是一个对象没有了根,就是它再也不被任何位置所引用,那么它就是垃圾的候选者了。性能

值得注意的一点是,对象可能在其生存期结束以前就被列入垃圾名单,甚至已经被GC所暗杀!那是由于对象可能在生存期的某一时刻已经再也不被引用,若是在这个时候执行垃圾收集,那么这个不幸的对象极有可能已经被列为垃圾并被销毁(为何说是“可能”呢?由于它不必定在GC的视力范围内。后面讲到“代龄”时会详细介绍相关细节)。this

1 publicstatic void Main()线程

2 {指针

3 string sGarbage= "I'm here";

4

5 //下面的代码没有再引用s,它已经成为垃圾对象---固然,这样的代码自己也是垃圾;

6 //此时若是执行垃圾收集,则sGarbage可能已经魂归西天

7

8 Console.WriteLine("Main() is end");

9 }

三. 对象代龄

尽管GC老是在默默为咱们劳动,但它毕竟是由人创造的,人会偷懒,它也会。为了减小每次的工做量,它老是但愿可以减小工做的范围;它坚信,越晚建立的对象每每越短命,所以它会集中精力处理这一部分的内存区域,暂且搁置其余部分。GC引入“代龄”的概念来划分对象生存级别。

CLR初始化后的第一批被建立的对象被列为0代对象。CLR会为0代对象设定一个容量限制,当建立的对象大小超过这个设定的容量上限时,GC就会开始工做,工做的范围是0代对象所处的内存区域,而后开始搜寻垃圾对象,并释放内存。当GC工做结束后,幸存的对象将被列为第1代对象而保留在第1代对象的区域内。此后新建立的对象将被列为新的一批0代对象,直到0代的内存区域再次被填满,而后会针对0代对象区域进行新一轮的垃圾收集,以后这些0代对象又会列为第1代对象,并入第1代区域内。第1代区域起初也会被设上一个容量限制值,等到第1代对象大小超过了这个限制以后,GC就会扩大战场,对第1代区域也作一次垃圾收集,以后,又一次幸存下来的对象将会提高一个代龄,成为第2代对象。

 

可见,有一些对象虽然符合垃圾的全部条件,但它们若是是第1代(甚至是第2代老臣)对象,而且第1代的分配量还小于被设定的限制值时,这些垃圾对象就不会被GC发现,而且能够继续存活下去。

另外,GC还会在工做过程当中汲取经验,根据应用程序的特色而自动调整每代对象区域的容量,从而能够更高效的工做。

应该了解的垃圾收集机制(二)

对于大多数应用而言,了解垃圾收集机制的主要动机并非为了对内存“省吃俭用”,而是为了处理非托管资源的控制问题,这些问题每每跟内存的大小没有什么关系。例如对一个文件进行操做,该什么时候关闭文件,关闭文件时要注意什么问题,若是忘了关闭会带来什么后果?这些都是咱们须要认真考虑的,不管你的内存有多大:)

对于这一类的操做,咱们不能依赖GC帮咱们作,由于它并不知道咱们在释放时想干什么,它甚至不知道本身该干什么!咱们不得不本身动手来编写处理代码。固然,微软已经为咱们搭好了框架,就是这两个函数:Finalize和Dispose。它们也表明了非托管清理的两种方式:自动和手动。

一. Finalize

Finalize很像C++的析构函数,咱们在代码中的实现形式为这与C++的析构函数在形式上彻底同样,但它的调用过程却大不相同。

~ClassName() {//释放你的非托管资源}

好比类A中实现了Finalize函数,在A的一个对象a被建立时(准确的说应该是构造函数被调用以前),它的指针被插入到一个finalization链表中;在GC运行时,它将查找finalization链表中的对象指针,若是此时a已是垃圾对象的话,它会被移入一个freachable队列中,最后GC会调用一个高优先级线程,这个线程专门负责遍历freachable队列并调用队列中全部对象的Finalize方法,至此,对象a中的非托管资源才获得了释放(固然前提是你正确实现了它的Finalize方法),而a所占用的内存资源则必需等到下一次GC才能获得释放,因此一个实现了Finalize方法的对象必需等两次GC才能被彻底释放。

因为Finalize是由GC负责调用,因此能够说是一种自动的释放方式。可是这里面要注意两个问题:第一,因为没法肯定GC什么时候会运做,所以可能很长的一段时间里对象的资源都没有获得释放,这对于一些关键资源而言是很是要命的。第二,因为负责调用Finalize的线程并不保证各个对象的Finalize的调用顺序,这可能会带来微妙的依赖性问题。若是你在对象a的Finalize中引用了对象b,而a和b二者都实现了Finalize,那么若是b的Finalize先被调用的话,随后在调用a的Finalize时就会出现问题,由于它引用了一个已经被释放的资源。所以,在Finalize方法中应该尽可能避免引用其余实现了Finalize方法的对象。

可见,这种“自动”释放资源的方法并不能知足咱们的须要,由于咱们不能显示的调用它(只能由GC调用),并且会产生依赖型问题。咱们须要更准确的控制资源的释放。

二. Dispose

Dispose是提供给咱们显示调用的方法。因为对Dispose的实现很容易出现问题,因此在一些书籍上(如《Effective C#》和《Applied Microsoft.Net Framework Programming》)给出了一个特定的实现模式:

class DisposePattern :IDisposable

{

private System.IO.FileStream fs = new System.IO.FileStream("test.txt", System.IO.FileMode.Create);

~DisposePattern()

{

Dispose(false);

}

IDisposable Members#region IDisposable Members

public void Dispose()

{

//告诉GC不须要再调用Finalize方法,

//由于资源已经被显示清理

GC.SupdivssFinalize(this);

Dispose(true);

}

#endregion

 

protected virtual void Dispose(bool disposing)

{

//因为Dispose方法可能被多线程调用,

//因此加锁以确保线程安全

lock (this)

{

if (disposing)

{

//说明对象的Finalize方法并无被执行,

//在这里能够安全的引用其余实现了Finalize方法的对象

}

if (fs != null)

{

fs.Dispose();

fs = null; //标识资源已经清理,避免屡次释放

}

}

}

}

在注释中已经有了比较清楚的描述,另外还有一点须要说明:若是DisposePattern类是派生自基类B,而B是一个实现了Dispose的类,那么DisposePattern中只须要override基类B的带参的Dispose方法便可,而不须要重写无参的Dispose和Finalize方法,此时Dispose的实现为:

class DerivedClass : DisposePattern

{

protected override void Dispose(bool disposing)

{

lock (this)

{

try

{

//清理本身的非托管资源,

//实现模式与DisposePattern相同

}

finally

{

base.Dispose(disposing);

}

}

}

}

固然,若是DerivedClass自己没有什么资源须要清理,那么就不须要重写Dispose方法了,正如咱们平时作的一些对话框,虽然都是继承于System.Windows.Forms.Form,但咱们经常不须要去重写基类Form的Dispose方法,由于自己没有什么非托管的咚咚须要释放。

了解GC的脾性在不少时候是很是必要的,起码在出现资源泄漏问题的时候你不至于手足无措。我写过一个生成excel报表的控件,其中对excel对象的释放就让我忙活了一阵。若是你作过excel开发的话,可能也遇到过结束excel进程之类的问题,特别是包装成一个供别人调用的库时,什么时候释放excel对象以确保进程结束是一个关键问题。固然,GC的内部机制很是复杂,还有许多内容可挖,但了解全部细节的成本过高,只需了解基础,够用就好。

相关文章
相关标签/搜索