.NET via C#笔记21——托管堆与垃圾回收

一、托管堆内存分配过程

A B C NextObjPtr
--- -- ---- --------------------
  1. 计算需要的内存大小
    1. 类型本身大小
    2. CLR对象需要的开销
      1. 型对象指针
      2. 同步块索引
  2. 检查NextObjPtr指向的空间是否足够
    1. 足够
      1. NextObjPtr处放置对象
      2. 清空分配的内存
      3. NextObjPtr赋值给this
      4. NextObjPtr移动到下一个可用位置
    2. 不够,进行一次GC
      1. 暂停所有线程
      2. 标记堆中所有对象为可删除
      3. 从栈和静态变量出发,遍历所有对象
      4. 销毁堆中剩余可删除对象
      5. 整理内存碎片,修改所有相关的引用指针
      6. 修改NextObjPtr
    3. GC后还是不够
      1. 抛出OutOfMemoryException

二、分代GC:提升性能

CLR提供0-2一共三个堆(逻辑上的),每个堆的大小有预算且是动态的。

分代GC基于如下假设

  1. 对象越新,生存期越长
  2. 对象越老,生存期越短
  3. 回收堆的一部分,速度比回收整个堆更快

新建一个对象的过程的lua伪代码

heap0, heap1, heap2

function Alloc(size)
    if heap0.avaliSpace < size then
        --对0号堆进行一次GC
        local activeObjs = heap0:Collect()
        -- 0号中堆剩下的对象紧凑排列后移入1号堆
        heap1:AddObjs(activeObjs)
        if heap1.avaliSpace < 0 then
            -- 对1号堆进行一次GC
            activeObjs = heap1:Collect()
            -- 1号堆剩下的对象紧凑排列后移入2号堆
            heap2:AddObjs(activeObjs)
            if heap2.avaliSpace < 0 then
                -- 回收二代堆,压缩存活对象
                heap2:Collect()
            end
        end
    end
    
    if heal0.avaliSpace >= size then
        return heap0:Alloc(size)
    else
        error('no enough space')
    else
end

在这一过程中,可能会动态调整各堆的预算。

三、大对象

  1. 大对象属于二代堆
  2. 大对象和小对象不在一起分配
  3. 大对象在GC后不会压缩

四、GC回收模式

  1. 客户端(低延迟),服务器(低资源占用)
  2. 并发(默认,有一个GC线程平时在收集不可达对象),非并发

五、回收不受托管堆管理的资源

  1. 在Finalize重写方法中定义
    1. 在对象真正释放才会调用
    2. 由独立线程运行
    3. 可能造成阻塞或死锁,无法解决
    4. 建议不要使用
  2. 使用SafeHandle包装native resource handle

使用IDisposable接口控制本地资源生存期

  1. 如果类的一个字段实现了IDisposable,那么类也需要实现,在Dispose方法中调用字段的Dispose方法
  2. 实现IDisposable接口的类中其他的方法和get property,建议在native resource释放后调用会抛出ObjectDisposedException
  3. 如果没有调用Dispose,记得在Finalize中释放本地资源
  4. 不建议对Dispose方法实现线程安全,而是在调用处保证没有其他线程在同时调用
  5. 正确使用Dispose方法的方式
    1. finally代码块中调用Dispose
      var fs = new FileStream("temp.dat", FileMode.Create);
      try {
          fs.Write(bytes, 0, bytes.Length);
      } finally {
          if (fs != null) fs.Dispose();
      }
      
    2. 使用using简化代码,两者是等价的
      using (var fs = new FileStream("temp.dat", FileMode.Create) {
          fs.Write(bytes, 0, bytes.Length);
      }
      

FileStreamStreamWriter

  1. FileStream包含一个缓冲区对象
  2. StreamWriter也包含一个缓冲区对象
  3. StreamWriter.Dispose对调用绑定FileStreamDispose方法
  4. StreamWriterFinalize函数中不再向FileStream中写入缓存的数据,会造成数据的丢失

Finalize的调用过程

当GC到拥有Finalize方法的对象时

  1. 移动到一个特殊的队列(对象又被引用住了)
  2. 提升到下一代堆
  3. 调用Finalize方法
  4. 等待下一次GC再回收这些对象

通过GCHandle类监控和控制对象的生命周期

public static GCHandle GCHandle.Alloc(object value, GCHandleType type);
  1. 使对象在内存中固定位置(不会GC和内存整理),调用Native函数
    var obj = new byte[1000];
    var handle = GCHandle.Alloc(obj, GCHandleType.Pinned);
    SomeNativeMethod(handle.ToIntPtr());
    // 异步使用完毕后,调用Free
    handle.Free();
    
    1. 使用P/Invoke时,会自动为参数固定住内存
    2. 可以使用fixed关键字进行内存固定(尝试了下发现必须在unsafe代码块中使用)
      var bytes = new byte[1000]
      fixed(byte* pBytes = bytes) {
          SomeNativeMethod((IntPtr)pBytes);
      }
      
  2. 弱引用住一个对象
    var obj = new object();
    var handle = GCHandle.Alloc(obj, GCHandleType.Weak);
    
    var obj = handle.Target;
    
    1. 可以使用WeakReference简化使用方法
原文地址:https://www.cnblogs.com/hamwj1991/p/12384395.html