一、托管堆基础
1,访问一个资源(文件、内存缓冲区、屏幕空间、网络连接、数据库资源等)所需的步骤
①调用IL指令newobj,为代表资源的类型分配内存(一般使用c# new操作符来完成)
②初始化内存,设置资源的初始状态并使资源可用。类型的实例构造器负责设置初始状态
③访问类型的成员来使用资源(有必要可以重复)
④摧毁资源的状态以进行清理
⑤释放内存。垃圾回收器独自负责这一步
2,从托管堆分配资源
初始化进程时,CLR划出一个地址空间区域作为托管堆,一个区域被非垃圾对象填满后,CLR会分配更多的区域(32位进程最多能分配1.5GB,64为进程最多能分配8TB)。CLR还要维护一个指针(NextObjPtr),该指针指向下一个对象在堆中的分配位值。刚开始的时候,NextObjPtr设为地址空间区域的基地址。
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(); }
二、代:提升性能
对象越新,生存期越短
对象越老,生存期越长
回收堆的一部分,速度快于回收整个堆
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(); }