本章的内容比较多,主题就是垃圾收集。讲述了托管应用程序如何构造对象,如何控制这些对象的生命期,这些对象占用的内存怎样回收。首先介绍了垃圾收集器的一些基本概念,然后解释了垃圾收集算法,接着讲了垃圾收集如何进行以及如何调试。重点讲了Finalization是怎么进行的,如何使用Finalizer来释放资源和SafeHandle的用法,通过继承自CriticalFinalizerObject类型来保证终止化被执行,隆重推出了Dispose模式。还介绍了C#中using的用法,如何手动的控制对象的生命期,如何复活一个对象。解释了代龄是什么,在使用本地资源时如何利用垃圾收集器的特性。还讲了如何通过使用MemoryFailPoint来提高内存敏感算法的健壮性。另外还讲了如何通过编程的方法来控制垃圾收集器运行,说明了什么是安全点(safe point)以及CLR怎样劫持线程以进行垃圾回收。最后讲了垃圾收集器的工作模式,CLR对待大对象的处理方法,以及怎样监视垃圾收集器的工作。
Understanding the Basics of Working in a Garbage-Collected Platform
- 可能泄露的系统资源(十有八九是句柄 handle):
- 用于窗口管理的用户对象:快捷键、输入状态符号(键盘指针 Carets)、鼠标光标(鼠标指针 Cursors)、系统钩子、图标、菜单、窗口;
- 用于图形的GDI对象:位图、画刷、设备环境(DC Device Context)、字体、内存DC、元文件、调色板、画笔、区域;
- 用于内存管理、进程执行和IPC的Kernel对象:文件、进程、线程、信号量、互斥体(Mutex)、定时器、访问令牌、套接字;
- 内存泄露的常见原因:
- 使用静态引用;
- 未退订的事件(※);
- 未退订的静态事件;
- 未调用Dispose方法;
- 使用不彻底的Dispose方法;
- 在Windows Forms中对BindSource的误用;
- 为在WorkItem(CAB)上调用Remove;
- .NET有垃圾收集,为什么还有内存泄露?Fabrice称,“内存泄漏发生在一块内存不再被使用,但却依然被程序所引用时。当一块内存无法被程序访问到时,垃圾收集器将会重新分配这块内存,但是如果程序仍然保持对内存的引用却不使用这块内存 时,就会造成内存泄漏”。
- 如何避免内存泄露:
- 对象的创建者或拥有者负责销毁对象,而不是使用者;
- 当不在需要一个事件订阅者时退订此事件,为确保安全可在Dispose中退订;
- 当对象不再触发事件时,应该将该对象设置为null并移除所有的事件订阅者;
- 当模型和视图引用同一个对象时,推荐给视图传递一个该对象的克隆,以防止无法追踪谁在使用哪个对象;
- 对系统资源的访问应该包装在using中。
- 对付内存泄露的工具:GDILeaks, dotTrace, .Net Memory Profiler, SOS.DLL, WinGbg;
- 访问资源的步骤:
- ILnewobj 分配内存;
- 初始化内存;
- 使用资源;
- 拆卸资源状态开始清理;
- 释放内存。
Allocating Resources from the Managed Heap
- CLR执行IL newobj 指令时的步骤:
- 计算类型和所有基类的字段需要的字节数;
- 加上对象头需要的字节数(一个类型对象指针、一个同步块索引)(32位应用:8字节;64位应用:16字节);
- CLR检查保留区中的空闲空间有没有能容纳该对象的,如果有移动NextObjPtr。如果没有,抛出异常OutOfMemoryException。
The Garbage Collection Algorithm
- 根:每个应用都有根;
- 对x86体系结构来说,CLR调用方法时前两个参数(L2R)通过ECX和EDX寄存器传递。所以对实例方法来说,this -> ECX;
- 垃圾标记阶段,如果没有根引用到该对象,那么该对象的同步块索引字段有一位会设置标记;
- 如果垃圾收集器遇到一个对象之前标记过垃圾,那么不再进行向下检查。原因:1. 增强性能;2. 避免死循环;
- 内存压缩阶段,将存活下来的对象移动到一起,避免内存碎片;
- 一个常见的内存泄露的原因是:有一个静态字段引用一个集合对象。尽可能的避免使用静态字段。
Garbage Collections and Debugging
- 编译器优化会对对象的生命期造成影响。
Using Finalization to Release Native Resources
- 终止化(Finalization)是CLR提供的允许对象在垃圾收集器回收对象内存之前能够优雅的执行清理工作的机制;
- 终止化语法:~TypeName(){};实际上C#编译器产生protected override Finalize()方法在模块的元数据中。
- Finalize不同于C++的析构器。
- CriticalFinalizerObject可以用来保证终止化,CLR给该类提供了3个重要的特性:
- 任何继承自CriticalFinalizerObject的对象第一次构造时,CLR立即JIT编译继承层次中的所有Finalize方法;
- CLR在调用非继承自CriticalFinalizerObject对象的Finalize方法之后,才会调用继承自CriticalFinalizerObject对象的Finalize方法;
- 如果主机应用程序粗鲁的中止了AppDomain,CLR会调用继承自CriticalFinalizerObject对象的Finalize方法。
- SafeHandle继承自CriticalFinalizerObject,是常用的Windows句柄资源的安全封装。并实现了Dispose模式;
- 在Windows中,无效句柄通常有0和-1两种表示。SafeHandleZeroOrMinusOneIsInvalid继承自SafeHandle;
- (SafeFileHandle, SafeRegistryHandle, SafeWaitHandle, SafeBuffer) > SafeHandleZeroOrMinusOneIsInvalid > SafeHandle > CriticalFinalizerObject;
- SafeHandle可以避免引用计数的安全漏洞。
Using Finalization with Managed Resources
- 在设计类型时,要尽可能的避免使用Finalize方法,原因多半跟性能有关:
- 可终止化的对象活的更久,因为需要把他们放到终止化列表(Finalization List)中去;
- 可终止化的对象会提升代龄,也是因为上一条的原因;
- 可终止化的对象会让你的应用跑的更慢因为每一个对象被回收时需要执行额外的操作。
- CLR不保证对象的Finalize方法以一定顺序执行;
- 应该尽可能的避免使用静态方法,因为会内部访问已经被终止化的对象,引发不可预知的行为。
What Causes Finalize Methods to Be Called?
- Finalize方法在垃圾收集完成的时候被调用;
- 能够引发Finalize方法被调用的事件:
- 0代堆空间已满;
- 代码显示的调用System.GC.Collect();
- Windows报告内存过低。CLR使用Win32函数CreateMemoryResourceNotification和QueryMemoryResourceNotification监控内存系统内存不足;
- CLR卸载一个AppDomain;
- CLR关闭。
- CLR使用一个特殊的专门线程来调用Finalize方法,以上的前四个事件,如果一个Finalize进入了死循环,那么该线程就会被阻塞,其他Finalize就不会被调用;
- 如果CLR关闭,每个Finalize方法有大约2秒的时间返回,如果超时那么CLR会咔嚓了该进程(其他Finalize方法就没机会执行了)。另外,所有对象的Finalize方法执行的时间总共不能超过40秒,否则CLR同样会咔嚓该进程;
- AppDomain.IsFinalizingForUnload()方法和System.Enviroment.HasShutdownStarted属性。
Finalization Internals
- 终止化列表(Finalization List),有Finalize方法的对象在创建时终止化列表会有一个指针指向它;
- 虽然System.Object有Finalize方法,但是CLR知道怎样忽视它;
- 终止可达队列(Freachable Queue),在一次垃圾收集之后,CLR会将需要回收的Finalizable对象从终止化列表中挪到终止可达队列中;
- 现在,执行Finalize的方法是单线程的,但是将来可能会变成多线程。也就是在Finalize方法中用到共享状态时要考虑使用线程同步锁;
- 虽然在一次垃圾收集时被标记为垃圾了,但是在Freachable Queue中又可达了,所以就不再是垃圾了。换个说法就是复活了;
- 在下一次垃圾收集完成后,终止可达队列中的对象才会真的被作为垃圾收集了。
The Dispose Pattern: Forcing an Object to Clean Up
- Finalize的一个问题就是它不是public方法,所以不能确保在什么时候被调用;
- public interface IDisposable { void Dispose(); }
-
/*
* SafeHandle.cs - Implementation of the
* "System.Runtime.InteropServices.SafeHandle" class.
*
* Copyright (C) 2004 Southern Storm Software, Pty Ltd.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
namespace System.Runtime.InteropServices
{
#if CONFIG_FRAMEWORK_2_0
using System.Runtime.ConstrainedExecution;
#else
using System.Runtime.Reliability;
#endif
#if CONFIG_FRAMEWORK_1_2
public abstract class SafeHandle
#if CONFIG_FRAMEWORK_2_0
: CriticalFinalizerObject, IDisposable
#else
: IDisposable
#endif
{
// Internal state.
protected IntPtr handle;
private bool ownsHandle;
private bool closed;
// Constructor.
protected SafeHandle(IntPtr invalidHandleValue, bool ownsHandle)
{
this.handle = invalidHandleValue;
this.ownsHandle = ownsHandle;
this.closed = false;
if(!ownsHandle)
{
GC.SuppressFinalize(this);
}
}
// Destructor.
~SafeHandle()
{
Destroy();
}
// Close this handle.
[ReliabilityContract(Consistency.WillNotCorruptState, CER.Success)]
public void Close()
{
Dispose();
}
// Perform a reference add.
[ReliabilityContract(Consistency.WillNotCorruptState, CER.MayFail)]
public void DangerousAddRef(ref bool success)
{
// Nothing to do in this implementation.
success = true;
}
// Get the handle.
[ReliabilityContract(Consistency.WillNotCorruptState, CER.Success)]
public IntPtr DangerousGetHandle()
{
return handle;
}
// Release the handle.
[ReliabilityContract(Consistency.WillNotCorruptState, CER.Success)]
public void DangerousRelease()
{
Destroy();
}
// Implement the IDisposable interface.
[ReliabilityContract(Consistency.WillNotCorruptState, CER.Success)]
public void Dispose()
{
Destroy();
}
// Release the handle.
[ReliabilityContract(Consistency.WillNotCorruptState, CER.Success)]
protected abstract bool ReleaseHandle();
// Set the handle.
[ReliabilityContract(Consistency.WillNotCorruptState, CER.Success)]
protected void SetHandle(IntPtr handle)
{
this.handle = handle;
}
// Set the handle to invalid.
[ReliabilityContract(Consistency.WillNotCorruptState, CER.Success)]
public void SetHandleAsInvalid()
{
this.closed = true;
}
// Determine if this handle is closed.
public bool IsClosed
{
[ReliabilityContract(Consistency.WillNotCorruptState,
CER.Success)]
get
{
return closed;
}
}
// Determine if this handle is invalid.
public abstract bool IsInvalid
{
[ReliabilityContract(Consistency.WillNotCorruptState,
CER.Success)]
get;
}
// Destroy this handle.
private void Destroy()
{
if(!IsClosed)
{
closed = true;
if(!IsInvalid)
{
ReleaseHandle();
GC.SuppressFinalize(this);
}
}
}
}; // class SafeHandle
#endif // CONFIG_FRAMEWORK_1_2
}; // namespace System.Runtime.InteropServices
- 调用System.GC.SuppressFinalize会给this参数关联对象设置一个标志位。当该标志位为ON时,CLR知道不要从Finalization List移动指针到Freachable Queue。防止对象的Finalize方法被调用,并确保直到下一次垃圾收集对象不会存活。
Using a Type That Implements the Dispose Pattern
- 避免直接调用对象的Dispose方法和Close方法。
C#'s using Statement
- 只要多个变量是同一个类型,那么using语句支持初始化多个变量;
- MutexLock,使用Dispose模式实现。
using System;
using System.Threading;
internal struct MutexLock : IDisposable {
private readonly Mutex _mutex;
public MutexLock(Mutex m) {
_mutex = m;
_mutex.WaitOne();
}
public void Dispose() {
_mutex.ReleaseMutex();
}
}
public static class Program {
public static void Main() {
Mutex m = new Mutex();
using ( new MutexLock(m) ) {
//Perform some thread-safe operation in here...
}
}
}
An Interesting Dependency Issue
- 使得垃圾收集器以特定的顺序终止化对象是不可能的,因为对象会互相引用;
- MDAs(Managed Debugging Assistants)。当MDA在VS IDE中打开时,.NET框架查找确定的公共编程错误并引发相应的MDA。
Monitoring and Controlling the Lifetime of Objects Manually
- CLR为每个AppDomain提供GC句柄表。GCHandle;
- public enum GCHandleType {Weak = 0, WeakTrackResurrection = 1, Normal = 2, Pinned = 3}
- Weak,允许你监控对象的生命期。当垃圾收集器已经确定对象在程序代码中不可达时,你可以检测出来;
- WeakTrackResurrection,允许你监控对象的生命期。当垃圾收集器已经确定对象在程序代码中不可达时,你可以检测出来。可以确定Finalize方法(如果有)已执行,并且对象的内存已经回收;
- Normal,允许你控制对象的生命期。可以告诉垃圾收集器一个对象必须保留在内存中即使没有根引用到它。当垃圾收集器工作时,该对象的内存可以会被压缩(移动)。这在CGHandle.Alloc方法中是默认的;
- Pinned,允许你控制对象的生命期。与Normal的区别是:当垃圾收集器工作时,该对象的内存可以不会被压缩(移动)。
- C#的关键fixed可以钉住一个对象;
- WeakReference是GCHandle的一个封装,逻辑上WeakReference构造器调用GCHandle.Alloc,Target属性调用GCHandle.Target,Finalize方法调用GCHandle.Free方法;
- WeakEventHandler,参考:译文:C#中的弱事件(Weak Events in C#)。
- System.Runtime.CompilerServices.ConditionalweakTable,.NET 4.0中引入。可以用来在SilverLight和WPF中实现依赖属性机制。
Resurrection
- 通常,复活不是个好主意,应该尽量避免此特性;
- GC.ReRegisterForFinalize,在终止化列表的末尾加入一个的新项。
Generations
- 代龄垃圾收集器(短暂的垃圾收集器)所基于的假定:
- 对象越新,生命期越短;
- 对象越老,生命期越长;
- 收集部分堆比收集整个堆要快的多。
- 0代目前是256K,1代是2M,2代的基本上就永生了;
- 为了避免过深的栈,应该避免使用递归方法。
Other Garbage Collection Features for Use with Native Resources
- GC.AddMemoryPressure()和HandleCollector.Add()内部都会调用GC.Collect(),在达到0代预算的开始点前强制一次垃圾收集。正常情况下,强制垃圾收集是强烈劝阻的,因为会给应用程序的性能带来严重的的负面影响。
Predicting the Success of an Operation that Requires a Lot of Memory
- MemoryFailPoint类可以用来执行一个内存需求很大的算法前检查是否有足够的内存;
- 如果MemoryFailPoint构造时没有抛出异常,你逻辑上保留这块请求的内存,然后你可以执行内存敏感的算法。但是,要注意你并没有物理上分配这块内存。这意味着仅仅是看上去你的算法成功的运行,获得了它所需的内存。MemoryFailPoint并不能保证你的算法获得所需的内存。这个类之所以存在,是为了帮助你生产更健壮的应用;
- 当你完全执行一个算法时,你需要调用你构造的MemoryFailPoint的Dispose方法。
Programatic Control of the Garbage Collector
- enum GCCollectionMode {Default, Forced, Optimized};
- GC.WaitForPendingFinalizers(),简单的挂起调用的线程直到线程处理已经消耗完的终止可达队列(Freachable Queue),调用每一个对象的Finalize方法;
- GC.GetGeneration()可以获得对象的代龄。
Thread Hijacking
- CLR核查每一个线程的指令指针来确定线程执行到哪里了。怎么确定?指令指针地址跟JIT的编译器产生的表来相比较;
- 安全点(safe point):是有这么一个地方,能够让一个线程可以安全的挂起直到垃圾收集完成;
- CLR劫持线程:CLR修改线程的栈使得返回地址指向一个CLR内部实现的特性函数。什么情况下?线程没有到达安全点,CLR不能执行垃圾收集。
Garbage Collection Modes
- 两种垃圾收集模式:
- Workstation:用于客户端应用,假定有运行在该机器上的不狂占CPU资源的其他应用。分两种情形:并发收集、非并发收集;
- Server:用于服务器应用,假定没有其他应用运行在该机器上,并且所有的CPU都可用来垃圾回收。
- 应用程序默认运行在Workstation模式下,并发收集开关打开;
- 配置文件<runtime><gcserver enabled="true" /></runtime>
- GCSettings类,可以查询GC的设置情况;
- 在并发收集模式下,垃圾收集器有一个附加的后台线程,在程序运行时并发的进行垃圾收集;
- 并发的垃圾收集提供给用户一种更好的交互体验,特别适合于交互型的CUI和GUI应用程序。但是并发收集会影响性能,主要因为需要更多的内存;
- 配置文件<runtime><gcConcurrent enabled="flase" /></runtime>
- enum GCLatencyMode {Batch, Interactive, LowLatency}; Batch和Interactive使用一个CER(Constrained Execution Region);
- 一般来说,垃圾收集器会避免收集2代的对象。当然,如果你调用GC.Collect(),2代的垃圾对象也会被收集。
Large Objects
- 任何超过85000字节的对象都视为大对象;
- 大对象的代龄直接是2代。
Monitoring Garbage Collections
- PerfMon.exe,CLR Profiler,SOS Debuging Extension(SOS.dll)。
本章小结
本章的内容比较多,主题就是垃圾收集。讲述了托管应用程序如何构造对象,如何控制这些对象的生命期,这些对象占用的内存怎样回收。首先介绍了垃圾收集器的一些基本概念,然后解释了垃圾收集算法,接着讲了垃圾收集如何进行以及如何调试。重点讲了Finalization是怎么进行的,如何使用Finalizer来释放资源和SafeHandle的用法,通过继承自CriticalFinalizerObject类型来保证终止化被执行,隆重推出了Dispose模式。还介绍了C#中using的用法,如何手动的控制对象的生命期,如何复活一个对象。解释了代龄是什么,在使用本地资源时如何利用垃圾收集器的特性。还讲了如何通过使用MemoryFailPoint来提高内存敏感算法的健壮性。另外还讲了如何通过编程的方法来控制垃圾收集器运行,说明了什么是安全点(safe point)以及CLR怎样劫持线程以进行垃圾回收。最后讲了垃圾收集器的工作模式,CLR对待大对象的处理方法,以及怎样监视垃圾收集器的工作。