一、应用程序对资源操做,一般简单分为如下几个步骤:为对应的资源分配内存 → 初始化内存 → 使用资源 → 清理资源 → 释放内存。程序员
二、应用程序对资源(内存使用)管理的方式,常见的通常有以下几种:web
[1] 手动管理:C,C++算法
[2] 计数管理:COM数据库
[3] 自动管理:.NET,Java,PHP,GO…编程
三、可是,手动管理和计数管理的复杂性很容易产生如下典型问题:windows
[1] 程序员忘记去释放内存安全
[2] 应用程序访问已经释放的内存服务器
产生的后果很严重,常见的如内存泄露、数据内容乱码,并且大部分时候,程序的行为会变得怪异而不可预测,还有Access Violation等。数据结构
.NET、Java等给出的解决方案,就是经过自动垃圾回收机制GC进行内存管理。这样,问题1天然获得解决,问题2也没有存在的基础。多线程
总结:没法自动化的内存管理方式极容易产生bug,影响系统稳定性,尤为是线上多服务器的集群环境,程序出现执行时bug必须定位到某台服务器而后dump内存再分析bug所在,极其打击开发人员编程积极性,并且源源不断的相似bug让人厌恶。
GC的工做流程主要分为以下几个步骤:
标记(Mark) → 计划(Plan) → 清理(Sweep) → 引用更新(Relocate) → 压缩(Compact)
目标:找出全部引用不为0(live)的实例
方法:找到全部的GC的根结点(GC Root), 将他们放到队列里,而后依次递归地遍历全部的根结点以及引用的全部子节点和子子节点,将全部被遍历到的结点标记成live。弱引用不会被考虑在内
[1] 计划
目标:判断是否须要压缩
方法:遍历当前全部的generation上全部的标记(Live),根据特定算法做出决策
[2] 清理
目标:回收全部的free空间
方法:遍历当前全部的generation上全部的标记(Live or Dead),把全部处在Live实例中间的内存块加入到可用内存链表中去
[1] 引用更新
目标: 将全部引用的地址进行更新
方法:计算出压缩后每一个实例对应的新地址,找到全部的GC的根结点(GC Root), 将他们放到队列里,而后依次递归地遍历全部的根结点以及引用的全部子节点和子子节点,将全部被遍历到的结点中引用的地址进行更新,包括弱引用。
[2] 压缩
目标:减小内存碎片
方法:根据计算出来的新地址,把实例移动到相应的位置。
本文反复出现的GC的根节点也即GC Root是个什么东西呢?
每一个应用程序都包含一组根(root)。每一个根都是一个存储位置,其中包含指向引用类型对象的一个指针。该指针要么引用托管堆中的一个对象,要么为null。
在应用程序中,只要某对象变得不可达,也就是没有根(root)引用该对象,这个对象就会成为垃圾回收器的目标。
用一句简洁的英文描述就是:GC roots are not objects in themselves but are instead references to objects.并且,Any object referenced by a GC root will automatically survive the next garbage collection.
.NET中能够看成GC Root的对象有以下几种:
一、全局变量
二、静态变量
三、栈上的全部局部变量(JIT)
四、栈上传入的参数变量
五、寄存器中的变量
注意,只有引用类型的变量才被认为是根,值类型的变量永远不被认为是根。由于值类型存储在堆栈中,而引用类型存储在托管堆上。
一、当应用程序分配新的对象,GC的代的预算大小已经达到阈值,好比GC的第0代已满;
二、代码主动显式调用System.GC.Collect();
三、其余特殊状况,好比,windows报告内存不足、CLR卸载AppDomain、CLR关闭,甚至某些极端状况下系统参数设置改变也可能致使GC回收。
代(Generation)引入的缘由主要是为了提升性能(Performance),以免收集整个堆(Heap)。一个基于代的垃圾回收器作出了以下几点假设:
一、对象越新,生存期越短;
二、对象越老,生存期越长;
三、回收堆的一部分,速度快于回收整个堆。
.NET的垃圾收集器将对象分为三代(Generation0,Generation1,Generation2)。不一样的代里面的内容以下:
一、G0 小对象(Size<85000Byte):新分配的小于85000字节的对象。
二、G1:在GC中幸存下来的G0对象
三、G2:大对象(Size>=85000Byte);在GC中幸存下来的G1对象
object o = new Byte[85000]; //large object Console.WriteLine(GC.GetGeneration(o)); //output is 2,not 0
这里必须知道,CLR要求全部的资源都从托管堆(managed heap)分配,CLR会管理两种类型的堆,小对象堆(small object heap,SOH)和大对象堆(large object heap,LOH),其中全部大于85000byte的内存分配都会在LOH上进行。
代收集规则:当一个代N被收集之后,在这个代里的幸存下来的对象会被标记为N+1代的对象。GC对不一样代的对象执行不一样的检查策略以优化性能。每一个GC周期都会检查第0代对象。大约1/10的GC周期检查第0代和第1代对象。大约1/100的GC周期检查全部的对象。
GC的开销一般很大,并且它的运行具备不肯定性,微软的编程规范里是强烈建议你不要显式调用GC。但你的代码中仍是可使用framework中GC的某些方法进行手动回收,前提是你必需要深入理解GC的回收原理,不然手动调用GC在特定场景下很容易干扰到GC的正常回收甚至引入不可预知的错误。
好比以下代码:
void SomeMethod() { object o1 = new Object(); object o2 = new Object(); o1.ToString(); GC.Collect(); // this forces o2 into Gen1, because it's still referenced o2.ToString(); }
若是没有GC.Collect(),o1和o2都将在下一次垃圾自动回收中进入Gen0,可是加上GC.Collect(),o2将被标记为Gen1,也就是0代回收没有释放o2占据的内存
还有的状况是编程不规范可能致使死锁,好比流传很广的一段代码:
public class MyClass { private bool isDisposed = false; ~MyClass() { Console.WriteLine("Enter destructor..."); lock (this) //some situation lead to deadlock { if (!isDisposed) { Console.WriteLine("Do Stuff..."); } } } }
经过以下代码进行调用:
var instance = new MyClass(); Monitor.Enter(instance); instance = null; GC.Collect(); GC.WaitForPendingFinalizers(); Console.WriteLine("instance is gabage collected");
上述代码将会致使死锁。缘由分析以下:
一、客户端主线程调用代码Monitor.Enter(instance)代码段lock住了instance实例
二、接着手动执行GC回收,主(Finalizer)线程会执行MyClass析构函数
三、在MyClass析构函数内部,使用了lock (this)代码,而主(Finalizer)线程尚未释放instance(也即这里的this),此时主线程只能等待
虽然严格来讲,上述代码并非GC的错,和多线程操做彷佛也无关,而是Lock使用不正确形成的。
同时请注意,GC的某些行为在Debug和Release模式下彻底不一样(Jeffrey Richter在<<CLR Via C#>>举过一个Timer的例子说明这个问题)。好比上述代码,在Debug模式下你可能发现它是正常运行的,而Release模式下则会死锁。
前面讨论的垃圾回收算法有一个很大的前提就是:只在一个线程运行。而在现实开发中,常常会出现多个线程同时访问托管堆的状况,或至少会有多个线程同时操做堆中的对象。一个线程引起垃圾回收时,其它线程绝对不能访问任何线程,由于垃圾回收器可能移动这些对象,更改它们的内存位置。CLR想要进行垃圾回收时,会当即挂起执行托管代码中的全部线程,正在执行非托管代码的线程不会挂起。而后,CLR检查每一个线程的指令指针,判断线程指向到哪里。接着,指令指针与JIT生成的表进行比较,判断线程正在执行什么代码。
若是线程的指令指针刚好在一个表中标记好的偏移位置,就说明该线程抵达了一个安全点。线程可在安全点安全地挂起,直至垃圾回收结束。若是线程指令指针不在表中标记的偏移位置,则代表该线程不在安全点,CLR也就不会开始垃圾回收。在这种状况下,CLR就会劫持该线程。也就是说,CLR会修改该线程栈,使该线程指向一个CLR内部的一个特殊函数。而后,线程恢复执行。当前的方法执行完后,他就会执行这个特殊函数,这个特殊函数会将该线程安全地挂起。然而,线程有时长时间执行当前所在方法。因此,当线程恢复执行后,大约有250毫秒的时间尝试劫持线程。过了这个时间,CLR会再次挂起线程,并检查该线程的指令指针。若是线程已抵达一个安全点,垃圾回收就能够开始了。可是,若是线程尚未抵达一个安全点,CLR就检查是否调用了另外一个方法。若是是,CLR再一次修改线程栈,以便从最近执行的一个方法返回以后劫持线程。而后,CLR恢复线程,进行下一次劫持尝试。全部线程都抵达安全点或被劫持以后,垃圾回收才能使用。垃圾回收完以后,全部线程都会恢复,应用程序继续运行,被劫持的线程返回最初调用它们的方法。
实际应用中,CLR大多数时候都是经过劫持线程来挂起线程,而不是根据JIT生成的表来判断线程是否到达了一个安全点。之因此如此,缘由是JIT生成表须要大量内存,会增大工做集,进而严重影响性能。
这里再说一个真实案例。某web应用程序中大量使用Task,后在生产环境发生莫名其妙的现象,程序时灵时不灵,根据数据库日志(其实还能够根据Windows事件跟踪(ETW)、IIS日志以及dump文件),发现了Task执行过程当中有不规律的未处理的异常,分析后怀疑是CLR垃圾回收致使,固然这种状况也只有在高并发条件下才会暴露出来。
因为GC的代价很大,平时开发中注意一些良好的编程习惯有可能对GC有积极正面的影响,不然有可能产生不良效果。
一、尽可能不要new很大的object,大对象(>=85000Byte)直接归为G2代,GC回收算法历来不对大对象堆(LOH)进行内存压缩整理,由于在堆中下移85000字节或更大的内存块会浪费太多CPU时间;
二、不要频繁的new生命周期很短object,这样频繁垃圾回收频繁压缩有可能会致使不少内存碎片,可使用设计良好稳定运行的对象池(ObjectPool)技术来规避这种问题
三、使用更好的编程技巧,好比更好的算法、更优的数据结构、更佳的解决策略等等
update:.NET4.5.1及其以上版本已经支持压缩大对象堆,可经过System.Runtime.GCSettings.LargeObjectHeapCompactionMode进行控制实现须要压缩LOH。
GC在一个独立的线程中运行来删除再也不被引用的内存。
Finalizer则由另外一个独立(高优先级CLR)线程来执行Finalizer的对象的内存回收。
对象的Finalizer被执行的时间是在对象再也不被引用后的某个不肯定的时间,并不是和C++中同样在对象超出生命周期时当即执行析构函数。
GC把每个须要执行Finalizer的对象放到一个队列(从终结列表移至freachable队列)中去,而后启动另外一个线程而不是在GC执行的线程来执行全部这些Finalizer,GC线程继续去删除其余待回收的对象。
在下一个GC周期,这些执行完Finalizer的对象的内存才会被回收。也就是说一个实现了Finalize方法的对象必需等两次GC才能被彻底释放。这也代表有Finalize的方法(Object默认的不算)的对象会在GC中自动“延长”生存周期。
特别注意:负责调用Finalize的线程并不保证各个对象的Finalize的调用顺序,这可能会带来微妙的依赖性问题(见<<CLR Via C#>>一个有趣的依赖性问题)。