//像背书一样,记录下吧
1.CLR分配资源
托管堆上维护着一个指针NextObjPtr.该指针表示下一个新建对象在托管堆上的位置.C#的new Object会产生IL newobj指令,NextObjPtr传递给this,构造完会返回对象的地址.
在托管堆中 连续分配的对象可以保证它们的地址也是连续的,可以得到性能方面的一些提升.
在托管堆上分配的对象,都有两个IntPtr的开销字段,一个是对象类型指针,二是同步块索引.在32位Application中都是4Byte=32bit,在64位Application上是64bit=
2.GC回收算法 & 回收过程 & 对象的代
垃圾收集器通过检查在托管堆中是否有应用程序不再使用的对象来回收内存.垃圾收集器是如何知道一个对象是否是不再使用的呢?
*其实回收的不是我们不再使用的对象,而是那些在当前作用域不能访问的对象.
应用程序的根:一个根即一个存储位置,包含着指向引用类型的对象指针,->托管堆中的对象 or null(空值).其实就是那些在当前代码/环境/context中可以访问的对象引用.如static 字段,局部变量,方法参数,CPU寄存器等
垃圾回收的过程
- 标记阶段 : GC开始工作的时候,将所有对象都认为是可收集的,然后从应用程序的根出发,看它引用了哪些对象,就在它们的同步块索引上标记一位,表示它还不能回收,然后再看这些被根引用的对象又引用了哪些对象,再次递归标记...知道找出所有可达的对象,标记出他们...
- 压缩阶段 : GC现在已将找出了所有可达的对象,GC会寻找连续的位被标记的内存区域(即会被回收的区域),如果找到较大的连续空间,GC会将 非垃圾对象 搬到这片连续的空间上,以压缩托管堆,如果碰到较小的内存块,GC会忽略不计.
那么GC怎么知道什么时候开始工作呢,来看对象的代
对象的代(generation)
托管堆中开始是空的,新添加的对象,被称为第0代,当应用程序占用内存很小时,没有必要去动用GC垃圾回收,况且这个垃圾回收可能使性能变差,但这个很小是什么概念,就是我们对分配对象占用的内存要有一个阀值,小于这个值,我们不需要GC工作,例如选择第0代对象256KB的预设空间,当分配的内存超出这个预设空间时,就会触发GC来回收内存.
经过第一次垃圾回收,有些对象没了,剩余存活的对象提升一个代,成为第一代,我们也为GC的第一代对象选择一个阀值,例如2MB.这时还在不断分配新对象,他们是第0代,当第0代的256KB满了之后,回收它,将存活的并入第1代...
经过不断回收,第1代的对象满了之后,GC会回收第1代,存活的提升为第2代,CLR via C#书上说第二代取10MB,第二代满了之后才会对第二代进行垃圾回收...
这样利用代的概念,减少GC回收要扫描的内存空间,减少GC对第1代,第二代扫描的频率.
3. 变量的存活期,以及调试器的干预
变量在不可达时就会在下一次垃圾回收时被回收.
来看个例子
1 public static void Main() 2 { 3 var timer = new System.Threading.Timer(delegate { 4 Console.WriteLine("timer elapsed at : " + DateTime.Now); 5 GC.Collect(); 6 },null,0,1000); 7 8 System.Console.ReadLine(); 9 }
这段代码,在VS 中Debug配置下正常工作(即按照预期,回调一直执行),在Realse配置下只会打印一行,即GC.Collect()将timer回收了
在Debug中为什么就不被回收呢?
是VS生成的程序集方便调试,将所有变量的存活期撑到方法结束,VS给程序集打上System.Diagnostics.DebuggableAttribute表示是在调试,并且在参数上指定禁用JIT优化代码
但是我们发布的时候可不能靠这个,我们得发布Realse版本,代码会被JIT优化,在上面的代码中,要保证对timer的引用,就不会被GC光顾~
修改代码
1 public static void Main() 2 { 3 var timer = new System.Threading.Timer(delegate { 4 Console.WriteLine("timer elapsed at : " + DateTime.Now); 5 GC.Collect(); 6 },null,0,1000); 7 8 System.Console.ReadLine(); 9 timer.Dispose();//保持对timer的引用 10 }
即可
4. Dispose Finalize函数,以及CriticalFinalizerObject
Dispose函数,手动调用,释放资源.
Finalize函数,在C#代码中,类的析构函数会被编译成终结函数(Finalize),供GC在垃圾回收该对象时自动调用
CriticalFinalizerObject类,在System.Runtime.ConstrainedExecution.CriticalFinalizerObject下面,该类没有任何实现,继承该类之后什么都不用做,会得到CLR and GC的特殊照顾.我们知道CLR在执行一个方法时JIT会将IL code编译成Native code,是到第一次执行时才编译,特么像解释型语言,但是CriticalFinalizerObject类型(包括派生类型)的Finalize方法会被立即编译,确保Finalize会被执行,本地资源会被释放.
同时在Finalize执行顺序上也有规则.CLR会先执行非CriticalFinalizerObject类的终结方法,后执行CriticalFinalizerObject类型的终结方法,因为这个特殊类型封装的本地资源最后调用析构函数,可以保证之前的终结方法调用时本地资源不被关闭
释放模式
在Winform项目里新建一个Form,打开Designer.cs文件,可以见到
Dispose()
{
Dispose(true);
}
void Dispose(bool disposing)
{
if(disposing)
{
//blabla
}
}
见到这种Code,即是释放模式:我们释放资源有两种情况,1是显式调用Dispose方法,2是GC在判定对象是要被垃圾回收时,调用终结函数
在IDisposable.Dispose和终结函数的实现中,我们均可以用Dispose(bool disposing)来实现
区别在于disposing参数,它表示是否是在显式调用Dispose()来释放资源,如果是,我们在本类中所引用的其他对象,可以确定他们可以访问,也就是还没被GC回收,可以在if(disposing)里回收这些对象,而GC在回收的时候,我们不确定在本类中所引用的对象是否已经被GC回收.公共部分可以释放一些GC无法处理的资源,如本地资源
5. FCL类库中的依赖,例 文件FIleStream 与 StreamWriter 依赖问题
1 var fs=new FileStream(path) 2 var sw=new StreamWriter(fs); 3 4 sw.Write(xxx)
StreamWriter在Write的时候,会写到自己的缓冲区,在缓冲区满的时候,或者Close的时候,写到FileStream,同理FileStream缓冲区满了之后写到文件.
MS没有给StreamWriter实现终结方法,如果数据存在缓冲区,而且没有close来关闭StreamWriter以Flush数据的话,数据就会丢失.
在关闭StreamWriter的时候,会关闭相关联的FileStream,而不用显式关闭