眼看着同学们一个个陆续收到了offer,好方啊。。
从托管堆分配资源
在谈GC之前我们先看一看CLR中资源是如何创建的。
CLR要求所有对象都从托管堆分配。进程初始化时,CLR划出一个地址空间区域作为托管堆。并维护一个指针,称之为NextObjPtr,这个指针指向下一个对象在堆中的分配位置。刚开始的时候,NextObjPtr为地址空间区域的基地址。怎么说呢,有些类似栈指针,每安排一个对象,栈顶指针就会移动一个对象的大小。
当然这个堆的大小是有上限的,如果超过了上限,即这个区域被非辣鸡对象占满,CLR会尝试分配更多的区域,或者尝试垃圾回收。直到整个进程地址空间都被填满,到那个时候会抛出OutOfMemoryException异常。
在创建对象时我们会使用new操作符,来看下使用new后在CLR中会执行哪些操作:
1.首先计算类型的字段(以及从基类型继承的字段)所需的字节数。
2.将这个字节数加上对象开销所需的字节数。每个对象有两个开销字段:类型对象指针和同步块索引。
类型对象指针在程序运行,JIT编译时会用到,用来引用和对象对应的基本类型。
同步块索引我暂时只发现在垃圾回收时有用(之后会讲),不知道有没有别的用途。
3.CLR检查区域中是否有分配对象所需的字节数。如果有,就在NextObjPtr指针指向的地址处放入对象,并将为对象分配的字节清零。然后调用类型的构造器(对象中的this的值就是NextObjPtr),接着new操作符返回对象引用,分配完成。
然后NextObjPtr的值会加上对象占用的总字节数得到一个新的值,即下次分配时的初始地址。
垃圾回收算法
应用程序调用new操作符创建对象时,可能没有足够的空间来分配。发现空间不够,CLR就执行垃圾回收操作。
.NET垃圾回收器采用的是mark-and-compact算法。这种算法只关心引用类型的变量,并将其成为根。
CLR开始垃圾回收时,首先暂停进程中的所有线程。这样可以防止线程在CLR检查期间访问对象并更改状态。然后CLR进入垃圾回收的mark阶段,这时CLR遍历堆中所有对象,将同步块索引中的一位设为0。这表明所有对象都应该删除。然后CLR检查所有活动根,查看它们引用了哪些对象。任何根如果引用了堆上的对象,表明这个对象时可达的(还有用的),CLR将这个对象的同步块索引中的位从0改为1。一个对象被标记后,CLR会检查那个对象中的根,标记它们引用的对象。如果发现对象已经标记,就不重新检查对象的字段,这就避免了因为循环引用产生的死循环。
检查完成后,堆中的对象就被分成0(不可达)和1(可达)两部分,接下来CLR就要处理那些不可达的对象。即进入compact阶段,这个过程类似于磁盘碎片整理,并不是删除不可达对象,而是通过移动可达的对象,覆盖掉不可达的对象。这种做法不仅减小了应用程序的工作集,也解决了堆的空间碎片化问题。
不过完成上述步骤后还有一个问题需要解决。引用幸存对象的根还是指向移动前的位置,就好比你已经搬了家,但别人知道的还是你之前的地址。所以CLR还要从每个根减去所引用的对象在内存中偏移的字节数。这样就能保证每个根的引用还是和之前一样的对象。
compact之后,NextObjPtr指针指向最后一个幸存对象之后的位置。CLR恢复所有线程的运行。
如果在一次垃圾回收之后回收不了内存,而且进程中没有空间来分配新的GC区域,就说明该进程的内侧已耗尽。此时,试图分配更多内存的new操作符会抛出OutOfMemoryException异常。这可能导致操作系统终止进程并回收内存,即程序崩溃_(:з」∠)_
代
.NET垃圾回收的特别之处在于引入了“代”的概念,并非所有垃圾都一定会在一个垃圾回收周期中被回收。它有以下几点假设:
1.对象越新,生存期越短。
2.对象越老,生存期越长。
3.回收堆的一部分,速度快与回收整个堆。
即垃圾回收期会以更快的频率尝试清除生存时间较短的对象(新对象)。
总共分了0,1,2三代。新生对象被列为0代,假如第一次垃圾回收没有清除掉它,他就会提升至1代,同理直到提升到2代。相较于2代的对象,垃圾回收器会以更快的频率对第0代的对象执行垃圾回收。
除此之外,还有个优化效率的方法。上文提到如果一个对象被标记后,CLR会检查那个对象中的根,标记它们引用的对象。如果这个根引用了老一代的某个对象,垃圾回收器就可以忽略老对象内部的所有引用,也就能在更短时间内构造好可达对象图。当然老对象的字段也有可能引用新对象,为了确保对老对象的已更新字段进行检查,垃圾回收器利用了JIT编译器内部的一个机制,这个机制在对象的引用字段发生变化时,会设置一个对应的位标识。这样,垃圾回收器就知道自上一次垃圾回收以来,那些老对象(如果有的话)已被写入,只有字段发生变化的老对象才需检查是否引用了第0代中的任何新对象。
垃圾回收触发条件
1.CLR检测到第0代的大小超出预算。
2.代码显式调用System.GC的静态Collect()方法。
尽量不要使用这种方法,因为在调用后会暂停所有线程的执行,所以降低性能。
不过在实现对性能要求高,多线程运行的方法前可以手动调用一次,尽量避免在方法运行时触发垃圾回收。
3.Windows报告低内存情况。
4.CLR卸载AppDomain。
5.CLR正在关闭。
GC与终结器
之前在q群看到有人问,既然有GC了为什么还需要终结器机制?
要回答这个问题首先要了解下托管资源和非托管资源:
- 托管资源指的是.NET可以自动进行回收的资源,主要是指托管堆上分配的内存资源。托管资源的回收工作是不需要人工干预的,有.NET运行库在合适调用垃圾回收器进行回收。
- 非托管资源指的是.NET不知道如何回收的资源,最常见的一类非托管资源是包装操作系统资源的对象,例如文件,窗口,网络连接,数据库连接,画刷,图标等。这类资源,垃圾回收器在清理的时候会调用Object.Finalize()方法。默认情况下,方法是空的,对于非托管对象,需要在此方法中编写回收非托管资源的代码,以便垃圾回收器正确回收资源。
简单说,GC只能处理堆上的资源,比如对象。而像文件句柄,数据库连接这些GC是无能为力的。需要我们手动释放它们,而终结器可以当做是保底机制,我们可以在终结器中写好清理非托管的资源的代码。假如我们忘记在代码中显式清理资源,当终结器被CLR调用的时候就可以帮我们清理。
参考书籍;
1.《CLR via C#(第4版)》
2.《C# 6.0 本质论》
3 comments
🙁 我不开心了
感谢博主,收益良多[此条5毛]
不客气不客气 👿