1、托管堆基础算法
1,访问一个资源(文件、内存缓冲区、屏幕空间、网络链接、数据库资源等)所需的步骤数据库
①调用IL指令newobj,为表明资源的类型分配内存(通常使用c# new操做符来完成)c#
②初始化内存,设置资源的初始状态并使资源可用。类型的实例构造器负责设置初始状态数组
③访问类型的成员来使用资源(有必要能够重复)安全
④摧毁资源的状态以进行清理服务器
⑤释放内存。垃圾回收器独自负责这一步网络
2,从托管堆分配资源并发
初始化进程时,CLR划出一个地址空间区域做为托管堆,一个区域被非垃圾对象填满后,CLR会分配更多的区域(32位进程最多能分配1.5GB,64为进程最多能分配8TB)。CLR还要维护一个指针(NextObjPtr),该指针指向下一个对象在堆中的分配位值。刚开始的时候,NextObjPtr设为地址空间区域的基地址。ide
3,C#的new操做符致使CLR执行如下步骤函数
①计算类型的字段(以及从基类型继承的字段)所需的字节数。
②加上对象的开销所需的字节数。每一个对象都有两个开销字段:类型对象指针和同步快索引(32位:两个字段各需32位,因此每一个对象要增长8字节。64位:每一个字段各需64位,因此每一个对象要增长16字节)(int=4字节;long=8字节)
③CLR检查区域中是否有分配对象所需的字节数。若是托管堆有足够的可用空间,就在NextObjPtr指针指向的地址处放入对象,为对象分配的字节会被清零。接着调用类型的构造器(为this参数传递NextObjPtr),new操做符返回对象的引用。就在返回这个引用以前,NextObjPtr指针的值会加上对象占用的字节数来获得一个新值,即下一个对象放入托管堆是的地址
4,垃圾回收算法
CLR使用一种引用跟踪算法。引用跟踪算法只关心引用类型的变量,由于只有这种变量才能引用堆上的对象,咱们将全部引用类型的变量都称为根。
①CLR开始GC时,首先暂停进程中的全部线程(这样能够防止线程在CLR检查期间访问对象并更改其状态)
②CLR进入GC标记阶段(这个阶段,CLR遍历堆中全部对象,将同步块索引字段中的一位设为0。这代表全部的对象都应该删除)
③CLR检查全部活动根(根为null,则CLR忽略这个根),查看他们引用了那些对象。若是引用了堆上的对象,CLR都会标记那个对象(将对象的同步块索引中的位设置为1)
④检查完毕后,堆中的对象要么标记。要么未标记。已标记的对象不能被垃圾回收,由于至少有一个根在引用它,咱们说这些对象时可达的
⑤进入GC的压缩阶段,在这个阶段,CLR对堆中已标记的对象进行“乾坤大挪移”,压缩全部幸存下来的对象,使它们占用连续的内存对象
⑥压缩以后,根如今的引用仍是原来的位置,而非移动以后的位置。因此做为压缩阶段的一部分,CLR还要从每一个根减去所引用的对象在内存中的偏移的字节数。这样就能保证根仍是引用和以前同样的对象;只是对象在内存中换了位置
5,垃圾回收和调试
①使用Release编译后,容许可执行文件,会发现TimerCallback方法只被调用了一次。由于Timer在初始化以后再也没有用过变量t。(调试模式下Timer对象不会被回收)
static void Main(string[] args) { //建立没2000毫秒就调用一次TimerCallback方法的timer对象 Timer t = new Timer(TimerCallback, null, 0, 2000); Console.ReadLine(); } private static void TimerCallback(object o) { Console.WriteLine("a"); //出于演示目的,强制执行一次垃圾回收 GC.Collect(); }
②显示要求释放计时器,它才能活到被释放的那一刻
static void Main(string[] args) { //建立没2000毫秒就调用一次TimerCallback方法的timer对象 Timer t = new Timer(TimerCallback, null, 0, 2000); Console.ReadLine(); //在ReadLine以后引用t(在Dispose方法返回以前,t会在GC中存活) t.Dispose(); } private static void TimerCallback(object o) { Console.WriteLine("a"); //出于演示目的,强制执行一次垃圾回收 GC.Collect(); }
2、代:提高性能
对象越新,生存期越短
对象越老,生存期越长
回收堆的一部分,速度快于回收整个堆
1,原理
①CLR初始化堆时为0代和1代选择预算容量(以kb为单位)。后期CLR会自动调节预算容量
②若是分配一个新的对象形成第0代超过预算,就必须启动一次垃圾回收
③通过垃圾回收以后,第0代的幸存者被提高到1代(第一代的大小增长);第0代又空了出来
④因为第0代已满,因此必须垃圾回收。但这一次垃圾回收器发现第1代用完了预算容量。因此此次垃圾回收器决定检查第1代和第0代的全部对象。两代被垃圾回收之后,第1代的幸存者提高到2代,第0代的幸存者提高到1代
2,垃圾回收触发的条件
①最多见触发条件:CLR在检查第0代超过预算时触发一次GC
②代码显示调用Sytem.GC的静态Collect方法
③Windows报告底内存状况
④CLR正在卸载AppDomain
⑤CLR正在关闭(CLR在进程正常终止时)
3,大对象
目前认为85000字节或更大的对象时大对象。(以前讨论的都是小对象)。大对象通常是大字符串(好比XML或JSON)或者用于I/O操做的字节数组(好比从文件或网络将字节读入缓冲区一遍处理)
①大对象不是在小对象的地址空间分配,而是在进程地址空间的其余地方分配
②目前版本的GC不压缩大对象,由于在内存中移动它们的代价太高
③大对象老是第2代,毫不多是第0代或者第1代
4,垃圾回收模式
CLR启动时会选择一个GC模式,进程终止前该模式不会变。
①两个主要模式:
1>工做站
该模式针对客户端应用程序优化GC。GC形成的延时很低,应用程序线程挂起时间很短,避免是用户感到焦虑。
2>服务器
该模式针对服务器应用程序优化GC。被优化的主要是吞吐量和资源利用。
②应用程序模式以“工做站”GC模式运行
③显示告诉CLR使用服务器回收站
<runtime> <gcServer enabled="true"></gcServer> </runtime>
//询问CLR它是否在“服务器”GC模式中运行 Console.WriteLine(GCSettings.IsServerGC); Console.ReadLine();
④两个子模式(并发(默认)或非并发)
在并发模式中,垃圾回收器有一个额外的后台线性,它能在应用程序运行时并发标记对象
<runtime> <!--告诉CLR不要使用并发回收器--> <gcConcurrent enabled="false"></gcConcurrent> </runtime>
⑤GCSettings的LatencyMode属性对垃圾回收进行某种程度的控制
符号名称 |
说明 |
Batch(“服务器”GC模式的默认值) |
关闭并发GC |
Interactive(“工做站”GC模式的默认值) |
打开并发GC |
LowLatency |
在短时间的、时间敏感的操做中(若是动画绘制)使用这个延迟模式。这些操做不适合对第二代进行回收 |
Sustained LowLatency |
使用这个延迟模式,应用程序的大多数操做都不会发生长的GC暂停。只要有足够的内存,它将禁止全部会形成阻塞的第二代回收操做。事实上,这种应用程序(例如须要迅速响应的股票软件)的用户应该考虑安装更多的RAM来防止发生生长的GC暂停 |
⑥正确的使用LowLatency
static void Main(string[] args) { GCLatencyMode oldModel = GCSettings.LatencyMode; Console.WriteLine(oldModel); //约束执行区域(CER) System.Runtime.CompilerServices.RuntimeHelpers.PrepareConstrainedRegions(); try { GCSettings.LatencyMode = GCLatencyMode.LowLatency; //在这里运行你的代码... } finally { GCSettings.LatencyMode = oldModel; } Console.ReadLine(); }
5,强制垃圾回收
public static void Collect(int generation, System.GCCollectionMode mode, bool blocking, bool compacting)
符号名称 |
说明 |
Default |
等同于不传递任何符号名称。目前还等同于Forced,但将来的版本可能对此进行修改 |
Forced |
强制回收指定的代(以及低于它的全部代) |
Optimized |
只有在能释放大量内存或者能减小碎片化的前提下,才执行回收。若是垃圾回收没有什么效率,当前调用就没有任何效果 |
若是写一个CUI(控制台用户界面)或GUI(图形用户界面)应用程序,你可能但愿建议垃圾回收的时间;为此,请将GCCollectionMode设置为Optimized并调用Collect。Default和Forced模式通常用于调试、测试和查找内存泄露
若是刚才发生了某个非重复性的事件,并致使大量旧对象死亡,就可考虑手动调用一次collect方法。因为是非重复性事件,垃圾回收器基于历史的预测可能不许确。因此,这是调用collect方法时合适的
//查看某一代发生了多少次垃圾回收 Console.WriteLine(GC.CollectionCount(0)); //查看托管堆中的对象当前使用了多少内存 Console.WriteLine(GC.GetTotalMemory(true));
3、使用须要特殊清理的类型
包含本机资源的类型被GC时,GC会回收对象在托管堆中使用的内存。但这样会形成本机资源的泄漏,因此CLR提供了称为终结的机制,容许对象在被断定为垃圾以后,但在对象内存被回收以前执行一些代码。任何包装了本机资源(文件、网络链接、套接字、互斥体)的类型都支持终结。CLR断定一个对象不可达时,对象将终结本身,释放它包装的本机资源。以后,GC会从托管堆回收对象
1,Finalize
它是为释放本机资源而设计的
internal sealed class SomeType { //这是一个Finalize方法 ~SomeType() { //这里的代码会进入Finalize方法 } }
2,SafeHandle
建立封装了本机资源的托管类型是,应该先从using System.Runtime.InteropServices.SafeHandle这个特殊基类派生一个类
public abstract class SafeHandle : CriticalFinalizerObject, IDisposable { //这是本机资源句柄 protected IntPtr handle; protected SafeHandle(IntPtr invalidHandleValue, Boolean ownsHandle) { handle = invalidHandleValue; //若是ownsHandle为true,那么这个从SafeHandle派生的对象被回收时,本机资源会被关闭 } protected SafeHandle(IntPtr invalidHandleValue) { handle = invalidHandleValue; } //显式释放资源 public void Dispose(){Dispose(true);} //默认的Dispose实现(以下所示)正是咱们但愿的。强烈建议不要重写这个方法 protected virtual void Dispose(Boolean disposing) { //这个默认实现会忽略disposing参数 //若是资源已经释放,那么返回 //若是ownsHandle为true,那么返回 //设置一个标志来指明该资源已经释放 //调用虚方法ReleaseHandle //调用GC.SuppressFinalize(this)方法来阻止调用Finalize方法 //若是ReleaseHandled返回true,那么返回 //若是走到这一步,就激活ReleaseHandleFailed托管调试助手(MDA) } //派生类型要从写这个方法以实现释放资源的代码 protected abstract Boolean ReleaseHandle(); //默认的Dispose实现(以下所示)正是咱们但愿的。强烈建议不要重写这个方法 ~SafeHandle(){Dispose(false);} public void SetHandleAsInvalid() { //设置标志来指出这个资源已经释放 //调用GC.SuppressFinalize(this)方法来阻止调用Finalize方法 } public Boolean IsClosed { get { //返回指出资源是否释放的一个标志} } public abstract Boolean IsInvalid { //派生类要重写这个属性 //若是句柄的值不表明资源(一般意味着句柄为0或-1),实现应返回true get; } //如下三个方法设计安全性和引用计数 public void DangerousAddRef(ref Boolean success){} public IntPtr DangerousGetHandle(){} public void DangerousRelease(){} }
CLR赋予这个类如下三个很酷的功能
①首次构造CriticalFinalizerObject派生类型对象时,CLR当即对继承层次结构中的全部Finalize方法进行JIT编译。构造对象时接编译这些方法,可确保放当对象被肯定为垃圾以后,本机资源确定会得以释放。不对Finalize方法进行提早编译,那么也许能分配并使用本机资源,但没法保证释放。内存紧张时,CLR可能找不到足够的内存来编译Finalize方法,这会阻止Finalize方法的执行,形成本机资源泄漏。另外,若是Finalize方法中的代码引用了另外一个程序集中的类型,但CLR定位该程序集失败,那么资源将得不到释放。
②CLR是在调用了非CriticalFinalizerObject派生类型的Finalize方法以后,才调用CriticalFinalizerObject派生类的Finalize方法。这样,托管资源类就能够在它们的Finalize方法中成功地访问CriticalFinalizerObject派生类型的对象。例如,FileStram类型的Finalize方法能够放心地将数据从内存缓冲区flush到磁盘,它知道此时磁盘文件尚未关闭
③若是AppDomain被一个宿主应用程序(例如Microsoft SQL Server或者Microsoft ASP.NET)强行中断,CLR将调用CriticalFinalizerObject派生类型的Finalize方法。宿主应用程序再也不信任它内部容许的托管代码,也利用这个功能确保本机资源得以释放。
3,SafeHandle派生类
SafeHandle派生类很是有用,由于它们保证本机资源在垃圾回收得以释放
internal static class SomeType { //这个原型不健壮 [DllImport("Kernal32",CharSet = CharSet.Unicode,EntryPoint = "CreateEvent")] private static extern IntPtr CreateEventBad(IntPtr pSecurityAttribute, Boolean manualReset, Boolean initialState, string name); //这个原型是健壮的 [DllImport("Kernal32", CharSet = CharSet.Unicode, EntryPoint = "CreateEvent")] private static extern SafeWaitHandle CreateEventGood(IntPtr pSecurityAttribute, Boolean manualReset, Boolean initialState, string name); public static void SomeMethod() { IntPtr handle = CreateEventBad(IntPtr.Zero, false, false, null); SafeWaitHandle swh = CreateEventGood(IntPtr.Zero, false, false, null); } }
4,使用包装了本机资源的类型
1>以FileStream为例,能够用它打开一个文件,从文件中读取字节,向文件中写入字节,而后关闭文件
①FileStream对象在构造时会调用Win32 CreateFile函数
②函数返回句柄保存到SafeFileHandle对象中
③而后经过FileStream对象的一个私有字段来维护对象的引用
2>FileStream的Dispose方法
①FileStream实现了IDisposable接口。FileStream的Dispose方法会调用SafeFileHandle字段上的Dispose方法。
②FileStream调用Dispose方法会清理本机资源。(并不是必定要调用Dispose才能保证本机资源得以清理。本机资源的清理最终总会发生,调用Dispose只是控制这个清理动做的发生时间)
③FileStream调用Dispose方法不会致使FileStram对象从托管堆中删除。只有在垃圾回收以后,托管堆中的内存才会得以回收
static void Main(string[] args) { //建立要写入临时文件的字节 byte[] bytesToWrite = new byte[] {1, 2, 3, 4, 5}; //建立临时文件 FileStream fs = new FileStream("Temp.dat", FileMode.Create); //将字节写入临时文件 fs.Write(bytesToWrite, 0, bytesToWrite.Length); //删除临时文件 File.Delete("Temp.dat");//抛出IOException异常 Console.ReadLine(); }
static void Main(string[] args) { //建立要写入临时文件的字节 byte[] bytesToWrite = new byte[] {1, 2, 3, 4, 5}; //建立临时文件 FileStream fs = new FileStream("Temp.dat", FileMode.Create); //将字节写入临时文件 fs.Write(bytesToWrite, 0, bytesToWrite.Length); //结束写入后显式关闭文件 fs.Dispose(); fs.Write(bytesToWrite, 0, bytesToWrite.Length);//抛出ObjectDisposedException //删除临时文件 File.Delete("Temp.dat");//抛出IOException异常 Console.ReadLine(); }
5,一个有趣的依赖性问题
//建立临时文件 FileStream fs = new FileStream("Temp.txt", FileMode.Create); StreamWriter sw = new StreamWriter(fs); sw.Write("abc"); //不要忘记这个Dispose的调用,不执行sw.Dispose()数据写不进文件 sw.Dispose(); //注意:调用StreamWriter.Dispose会关闭FileStream; //FileStream对象无需显示关闭
不须要再FileStream对象上显式调用Dispose,由于StreanWrite会帮你调用。但若是非要显式调用Dispose,FileStream会发现对象已经清理过了,因此方法什么都不作直接返回
6,终结器的内部工做原理
①应用程序建立新对象时,New操做符会从推中分配内存。若是对象的类型定义了Funalize方法,那么在该类型的实例构造器被调用以前,会将指向该对象的指针放到一个终结列表中
②垃圾回收时,对象B,D,E,F断定为垃圾。垃圾回收器扫描终结列表以查找这些对象的引用。找到一个引用以后,该引用从终结列表中移除,并附加到freachable队列中
③一个特殊的高优先级CLR线程专门调用Finalize方法。一旦freachable队列中有记录项出现,线程就会唤醒,将每一项都从freachable队列中移除,同时调用每一个对象的Finalize方法。
④下一次对老一代垃圾回收时,会发现已终结的对象成为真正的垃圾,由于没有应用程序的根指向它们,freachable队列也再也不指向它们,因此,这些对象的内存会直接回收
(注意:CLR会忽略System.Object定义的Finalize方法)
(注意:可终结对象须要执行两次垃圾回收才能释放它们的内存。在实际应用中,因为对象可能被提高至另外一代,因此可能要求不止进行两次垃圾回收)
7,手动监视和控制对象的生存期
public struct GCHandle { //静态方法,用于在表中建立一个记录项 public static GCHandle Alloc(object value); public static GCHandle Alloc(object value,GCHandleType type); //静态方法,用于将一个GCHandle转成一个IntPtr public static explicit operator IntPtr(GCHandle value); public static IntPtr ToIntPtr(GCHandle value); //静态方法,用于将一个IntPtr转成一个GCHandle public static explicit operator GCHandle(IntPtr value); public static GCHandle FromIntPtr(IntPtr value); //实例方法,用于释放表中的记录项(索引设置为0) public void Free(); //实例属性,用于get/set记录项的对象引用 public object Target { get; set; } //实例属性,若是索引不为0,就放回true public Boolean IsAllocated { get; } //对于已固定(pinned)的记录项,这个方法返回对象的地址 public IntPtr AddrOfPinnedObject(); }
public enum GCHandleType { Weak = 0, //监事对象的存在 WeakTrackResurrection = 1, //监事对象的存在 Normal = 2, //控制对象的生存期 Pinned = 3 //控制对象的生存期 }
Weak:
该标志容许监视对象的生存期。可检测垃圾回收器再何时断定该对象在应用程序代码中不可达。注意,此时对象的Finalize方法可能执行,也可能没有执行,对象可能还在内存中。
WeakTrackResurrection:
该标志容许监视对象的生存期。可检测垃圾回收器在何时断定该对象在应用程序的代码不可达。注意,此时对象的Finalize方法(若是有的话)已经执行,对象的内存已经回收
Normal:
该标志容许控制对象的生存期。告诉垃圾回收器:即便应用程序中没有根引用对象,该对象也必须留在内存中。垃圾回收发生时,该对象的内存能够压缩(移动)。Alloc方法默认的标志
Pinned:
该标志容许控制对象的生存期。告诉垃圾回收器:即便应用程序中没有根引用对象,该对象也必须留在内存中。垃圾回收发生时,该对象的内存不压缩(移动)。须要将内存地址交给本机代码时,这个功能很好用。本机代码知道GC不会移动对象,因此能放心地向托管堆的这个内存写入。
1>垃圾回收器如何使用GC句柄表。当垃圾回收发生时,垃圾回收器的行为以下①垃圾回收器标记全部可达的对象。而后。垃圾回收器扫描GC句柄表;全部Normal或Pinned对象都被当作是根,同时标记这些对象(包括对象经过他们的字段引用的对象)②垃圾回收器扫描GC句柄表,查找全部Weak记录项。若是一个Weak记录项引用了未标记的对象,该引用标识的就是不可达对象(垃圾),记录项的引用值更改成null③垃圾回收器扫描终结列表。在列表中,对未标记对象的引用标识的是不可达对象,这个引用从终结列表移至freachable队列,这是对象会被标记,由于对象又变成可达了④垃圾回收器扫描GC句柄表,查找全部WeakTrackResurrection记录项。若是一个WeakTrackResurrection记录项引用了未标记的对象(它如今是有freachable队列中的记录项引用的),该引用标识的就是不可达对象(垃圾),该记录项的引用值更改成null⑤垃圾回收器对内存进行压缩,填补不可达对象留下的内存“空调”,这其实就是一个内存碎片整理的过程。Pinned对象不会压缩(移动),垃圾回收器会移动它周围的其余对象