使用需要特殊清理的类型

.

StreamWriter和FileStream的一个问题讨论

System.IO.FileStream类型允许用户打开文件进行读写。为提高性能,该类型的实现利用了一个内存缓冲区只有缓冲区满时,类型才将缓冲区中的数据刷入文件。FileStream类型只支持字节的写入。写入字符和字符串可以使用一个System.IO.StreamWriter,如下所示

FileStream fs=new FileStream("temp.dat",FileMode.Create);
StreamWriter sw=new StreamWriter(fs);
sw.Write(“Hi There”);
// 不要忘记写下面这个Dispose调用
// 由于StreamWriter对象实现了IDisposable接口, 所以可以使用C#的using语句
// 调用Dispose,StreamWriter对象将数据flush到Stream对象并关闭该对象
sw.Dispose();
// 调用StreamWriter 的Dispose会关闭FileStream
// FileStream无需显示关闭
// 不需要再FileStream上显式调用Dispose,因为StreamWriter会帮你调用, 非要显式调用Dispose
// FileStream会发现对象已经清理过了, 所以方法什么都不做而直接返回.

不需要再FileStream上显式调用Dispose,因为StreamWriter会帮你调用, 非要显式调用Dispose,FileStream会发现对象已经清理过了, 所以方法什么都不做而直接返回.

没有代码显式调用Dispose会发生什么? 在某个时刻,垃圾回收器会正确检测到对象是垃圾,并对其进行终结, 但垃圾回收器不保证对象的终结顺序. 所以, 如果FileStream对象先终结, 就会先关闭文件,然后StreamWriter对象终结时,会试图向已关闭的文件flush(冲入,写入)数据, 造成抛出异常. 如果StreamWriter对象先终结,数据就会安全写入文件.

Microsoft解决这个问题是让StreamWriter对象类型不支持终结器, 如果不调用Dispose方法,就永远不会将缓冲区的数据flush到FileStream对象. 数据肯定丢失.

GC为本机资源提供的其他功能

包装本机资源的托管对象只占用很少的内存,但是本机资源有时会消耗大量内存.例如位图, 如果大量分配数百个位图(CLR觉得这些对象占用的内存很少不需要去GC),但进程的内存将以一个恐怖的速度增长.

为了修正这个问题,GC类提供了以下2个静态方法:

public static void AddMemoryPressure(Int64 bytesAllocated);
public static void RemoveMemoryPressure(Int64 bytesAllocated);

如果一个类要包装可能很大的本机资源,就应该使用这些方法提示垃圾回收器实际需要消耗多少内存. 垃圾回收器会监视内存压力, 压力变大时,就强制执行垃圾回收.

详细例子可以去看原文.

终结的内部工作原理

终结器表面上很简单: 创建对象, 当它被回收时, 它的Finalize方法得以调用. 深究下去, 就会发现终结的门道很多.

  1. new操作符会从堆中分配内存
  2. 如果对象的类型重写System.Object继承的Finalize方法(就认为此类和派生类是可终结对象).
    1. 不重写,就会被CLR认为忽略掉.认为是不可终结对象.
  3. 类型构造器被调用之前,会将该对象指针放入 终结列表(finalization list)
    1. 这个终结列表是由垃圾回收器孔子的一个内部数据结构.
  4. 回收对象的内存前,应调用该对象的Finalize方法.

系统检测到CEFIJ对象定义了Finalize方法, 所以将指向这些对象的指针添加到终结列表中.

垃圾回收开始时,对象BEGHIJ被判定为垃圾, 垃圾回收器扫描终结队列查找这些对象的引用, 找到后从终结列表移除, 移除并附加到freachable队列(也是垃圾回收器的一个内部数据结构)

freachable 名字的由来: f 代表终结 finalization . freachable队列中的每个记录项都是对托管堆中应调用其Finalize方法的一个对象的引用. reachable 代表对象是可达的.
换言之: 可将freachable队列看成是像静态字段那样的一个根(引用对象). 所以freachable队列中的引用使它指向的对象保持可达,不是垃圾.

BGH占用的内存已经被回收, EIJ占用的内存暂时不能回收, 因为它们的Finalize方法还没有调用.

一个特殊的高优先级CLR线程专门调用Finalize方法. freachable队列为空时,线程睡眠,一但进来记录项,就会唤醒线程去执行, 然后调用Finalize方法后移除. 注意不应该在Finalize中访问线程的本地存储.

当一个对象不可达时, 垃圾回收器就视它为垃圾. 但是当垃圾回收器将对象引用从终结列表移至freachable队列时, 就不再被认为是垃圾, 不能回收它的内存. 对象复活了…

标记freachable对象时, 垃圾回收器将递归标记对象中的引用类型的字段所引用的对象; 所以这些对象也必须复活以便在回收过程中存活. 之后, 垃圾回收器才结束对垃圾的标识. 在这个过程中, 一些原本被认为是垃圾的对象复活了,然后垃圾回收器压缩(移动)可回收的内存, 将复活的对象提升到较老的一(这不理想), 现在,特殊的终结线程清空freachable队列,执行每个对象的Finalize方法.

在下一次对老一进行回收时,会发现已终结的对象称为真正的垃圾, 因为没有指向他们. freachable队列也不再指向它们. 所以在整个过程中, 可终结对象需要执行两次垃圾回收才能释放它们占用的内存. 在实际应用中可能不止两次, 可能被提升至另一了.

手动监视和控制对象的生存期

CLR为每个AppDomain都提供了GC句柄表(GC Handle table), 允许应用程序监视或手动控制对象的生存期. 这个表创建之初是空白的, 其中的记录项包含2中信息:

  • 对托管堆中的一个对象的引用
  • 如何监视或控制对象的标志flag

简单地说,为了控制或监视对象的生存期, 可调用GCHandle静态Alloc方法,并传递想控制/监视的对象的引用. 还可传递一个GCHandleType枚举标志,定义如下:

public enum GCHandleType
{
   Weak = 0,                  // 用于监视对象的存在
   WeakTrackResurrection = 1, // 用于监视对象的存在
   Normal = 2,                // 用于控制对象的生存期
   Pinned = 3                 // 用于控制对象的生存期
}
  • Weak : 监视对象的生存期, 具体地说,可以检测垃圾回收器在什么时候判断该对象在应用程序代码中不可达. 注意, 此时对象的Finalize方法可能执行,也可能没执行, 对象可能还在内存中.
  • WeakTrackResurrection : 与上面相同, 不同的是此时对象的Finalize方法已经执行,对象的内存已经回收.
  • Normal : 该标志允许控制对象的生存期. 是告诉垃圾回收器: 即使应用程序中没有变量(根)引用该对象. 该变量也必须留在内存中. 垃圾回收发生时, 该对象的内存可以压缩(移动). 不向Alloc方法传递任何GCHandleType标志.就默认用Normal.
  • Pinned : 与上面那条不同的是, 垃圾回收发生时, 该对象的内存不能压缩(移动). 这个功能用于需要将内存地址交给本机代码时,本机代码知道GC不会移动对象,所以能放心地向托管堆的这个内存写入.

GCHandle静态Alloc方法会在调用时扫描AppDomain的GC句柄表,调用GCHandleFree方法, 将IntPtr字段设为0, 使实例变得无效.

垃圾回收器如何使用GC句柄表, 当垃圾回收发生时, 垃圾回收器的行为如下:

在什么情况下使用, 具体看书.

fix语句的使用

关于强弱引用

开发人员刚开始学习弱引用时, 会马上想到它们在缓存情形中的用处. 例如: 构造包含大量数据的一组对象, 并创建这些对象的弱引用. 需要数据时就去检查这些弱引用, 看看包含这些数据的对象是否依然”健在”, 对象还在就直接使用对象; 这与程序就会有较好的性能, 但是如果发生垃圾回收, 包含数据的对象就会被销毁. 一旦需要重新创建数据,性能就会收到影响.

垃圾回收不是内存满或接近满时才发生的, 只要第0代满了,垃圾回收就会发生.

弱引用在缓存情形中确实能得到高效应用,但构建良好的缓存算法来找到内存消耗和速度之间的平衡点十分复杂. 简单的说: 你希望缓存保持对自己的所有对象的强引用, 内存吃紧就开始将强引用转换为弱引用. 通过调用WIN32的GlobalMemoryyStatusEx函数并检查返回MEMORYSTATUSEX结构的dwMemoryLoad成员做到这一点. 当大于80,内存空间处于吃紧状态.