在介绍GC前,有必要对.net中CLR管理内存区域作简要介绍:java
一、 堆栈:用于分配值类型实例。堆栈主要操做系统管理,而不受垃圾收集器的控制,当值类型实例所在方法结束时,其存储单位自动释放。栈的执行效率高,但存储容量有限。算法
2 、GC堆:用于分配小对象实例。若是引用类型对象实例的大小小于85000字节,实例将被配置在GC堆上,当有内存分配或者回收时,垃圾收集器可能会对GC堆进行压缩。数据库
三、 LOH:large object heap,用于分配大对象实例。若是引用类型对象的实例的大小不小于85000字节时,该实例将被分配到LOH堆上,而LOH堆不会被压缩,并且只在彻底GC回收时被回收。c#
在面向对象的环境中,每一个类型都表明可供程序使用的一种资源,要使用这些资源,必须为表明资源的类型分配内存,如下是访问一个资源所需的步骤:缓存
1 调用IL指令newobj,为表明资源的类型分配内存(通常使用c#new操做符来完成);性能优化
2 初始化内存,设置资源的初始化状态并使资源可用。类型的实例构造器负责设置初始化状态;服务器
3 访问类型的成员来使用资源(有必要可用重复);数据结构
4 摧毁资源的状态以进行清理;(大多数时候不须要,通常只有包含了本机资源—文件、套接字、数据库链接等的类型才须要特殊清理,dispose或finalize)多线程
5 释放内存。垃圾回收器独自负责这一步。并发
NextObjPtr:该指针指向下一个对象在堆中的分配位置;刚开始时,NextObjPtr设为地址空间区域的基地址。
C#的new操做符致使CLR执行如下步骤:
1 计算类型的字段所需的字节数;
2 加上对象的开销所需的字节数。每一个对象都有两个开销字段:类型对象指针和同步快索引。对应32位应用程序,这两个字段各自须要32位,因此每一个对象须要增长8个字节。对应64位应用程序,两个字段各自须要64位,因此每一个对象须要增长16字节。
3 CLR检查区域中是否有分配对象所需的字节数。若是托管堆有足够的可用空间,就在NextObjPtr指针指向地址处放入对象,为对象分配的字节会被清零。接着调用类型的构造器(为this参数传递NextObjPtr),new操做符返回对象引用。就在返回这个引用以前,NextObjPtr指针的值会加上对象占用的字节数来获得一个新值,即下个对象放入托管堆时的地址。
局部性原理:cpu访问存储器时,不管存取指令仍是存取数据,所访问的存储单元都趋于汇集在一个较小的连续区域中。
对于托管堆,分配对象只须要在指针上加一个值,速度至关快。在许多应用程序中,差很少同时分配的对象彼此间有较强的联系,并且常常差很少在同一时间访问。因为托管堆在内存中连续分配这些对象,因此会由于引用的“局部化”locality而得到性能上的提高。具体来讲,这意味着进程的工做集会很是小,应用程序只需使用不多的内存,从而提升了速度。还意味着代码使用的对象能够所有驻留在CPU的缓存中,结果是应用程序能以惊人的速度访问这些对象,由于cpu在只需大多数操做时,不会由于“缓存未命中”cache miss而被迫访问较慢的RAM。
既然要清理垃圾,那么必然要明白什么是垃圾吧,垃圾的理解:一个对象成为“垃圾”表示该对象不被任何其余对象所引用。所以GC必须采用必定的算法在托管堆中遍历全部对象,最终造成一个可达对象图,而不可达的对象将成为被释放的垃圾对象等待收集。
在明白了什么是垃圾后,确定会对GC如何回收垃圾提出疑问。.net平台下,每一个应用程序都有一组根(指针),它指向托管堆中的存储位置,由JIT编译器和CLR运行时维护根指针列表,主要包括全局变量、静态变量、局部变量和寄存器指针等。GC正是经过根指针列表来得到托管堆中的对象图,其中定义了应用程序根引用的托管堆中的对象,当GC启动时,它假设全部对象都是可回收的垃圾,开始遍历全部的根,将根引用的对象标记为可达对象添加到可达对象图中,在遍历过程当中,若是根引用的对象还引用着其余对象,则该对象也被添加到可达对象图中,依次类推,GC经过根列表的递归遍历,将能找到全部可达对象,并造成一个可达对象图。同时那些不可达对象则被认为是可回收对象,GC接着运行垃圾收集进程来释放垃圾对象的内存空间。这种收集算法称为:标记和清除收集算法。
引用计数算法:
对象生存期的管理,有的系统采用的是某种引用计数算法。在这类系统中,堆上的每一个对象都维护这一个内存字段来统计程序中多少“部分”正在使用对象,随着每个“部分”到达代码中某个再也不须要对象的地方,就递减对象的计数字段,直至计数字段变为0,对象就能够从内存中删除了。引用计数算法最大的问题是处理很差循环引用。
引用跟踪算法:
鉴于引用计数算法存在的问题,CLR改成使用一种引用跟踪算法。引用跟踪算法只关心引用类型的变量,由于只有这种变量才能引用堆上的对象;值类型变量直接包含值类型实例。引用类型变量可在许多场合使用,包括类的静态和实例字段,或者方法的参数和局部变量。咱们将全部引用类型的变量都成为根。CLR开始GC时,首先暂停进程中的全部线程,这样能够防止线程在CLR检查期间访问对象并更改其状态。而后CLR进入GC标记阶段,其中,CLR遍历堆中全部的对象,将同步块索引字段中的一位设为0,代表全部对象都应删除。而后CLR检查全部的活动根,查看它们引用了哪些对象。若是一个根包含null,CLR忽略这个根并继续检查下个根。任何根若是引用了堆上的对象,CLR都会标记那个对象,也就是将该对象的同步块索引中的位设为1.一个对象被标记后,CLR会检查那个对象中的根,标记它们引用的对象。若是发现对象已经标记,就不从新检查对象的字段,这就避免了由于循环引用而产生的死循环了。
检查完毕后,堆中的对象要么已标记,要么未标记。已标记的对象不能被垃圾回收,引用这说明至少有一个对象在引用它,该对象时可达的reachable。CLR在检查完毕后,进入GC压缩compact阶段。压缩幸存的对象,使他们占用联系的内存空间。好处有:1 幸存对象在内存中紧挨着,恢复了引用的“局部化”,减少了应用程序的工做集,提高访问这些对象时的性能;2 压缩也可形成可用空间也所有是连续的,这段地址空间获得解放,容许其余对象入住,意味着压缩托管堆也解决了本机(原生)堆的空间碎片化问题。压缩后,引用幸存对象的根如今引用的仍是对象最初在内存中的位置,因此CLR还要从每一个根减去所引用的对象在内存中偏移的字节数,这样就能保证每一个根的引用仍是和以前同样的对象,只是对象在内存中变换了位置。
静态字段引用的对象一直存在,直到用于加载类型的AppDomain卸载为止。内存泄露的一个常见缘由是让静态字段引用某个集合对象,而后不停地向集合添加数据项。静态字段使集合对象一直存活,而集合对象使全部数据项一直存活。所以,尽可能避免使用静态字段。
CLR的GC是基于代的垃圾回收器,对于大多数应用程序:
对象越新,生存期越短;
对象越老,生存期越长;
回收堆的一部分,速度快于回收整个堆。
添加到堆的新对象会分配到第0代中,若是超过预算,就必须启动一次垃圾回收,压缩幸存的对象成为第1代对象,一次垃圾回收后,第0代就不包含任何对象了。若是再有新对象,仍是会分配到第0代中。当系统在分配新对象时若是第0代超出预算,形成必须启动垃圾回收。开始垃圾回收时,GC必须决定检查哪些代,CLR初始化时会为第0代对象选择预算,事实上,它还必须为第1代选择预算。开始垃圾回收时,根据越新的对象活的越短。所以第0代包含更多的垃圾,能回收更多的内存,CLR通过计算,若是第1代占用的内存远少于预算,GC只检查第0代的对象,忽略第1代中的对象,加快了垃圾回收速度,提高GC的性能,但对性能更大的提高做用是没必要遍历托管堆中的每一个对象,若是根或者对象引用了老一代的某个对象,GC就能够忽略老对象内部的全部引用,能在更短的时间内构造号可达对象图。固然老对象的字段也有可能引用新对象,为了确保对老对象的已更新字段进行检查,GC利用了JIT编译器内部的一个机制,该机制在对象的引用字段发生变化是,会设置一个对应的位标志,这样,GC就知道自上一次垃圾回收以来,那行老对象已被写入,只有字段发生变化的老对象才需检查是否引用了第0代中的任何新对象。
在通过一次次的垃圾回收后,第0代的幸存者提高为第1代,第1代的幸存者提高至第2代。有可能虽然在屡次垃圾回收后,但只有第1代超出预算时才会检查第1代中的对象。托管堆只支持三代。CLR初始化时,会为每一代选择预算,然而,CLR的垃圾回收器是自调节的。若是GC发现回收0代后存活下来的对象不多,就可能减少第0代的预算,分配空间减小意味着垃圾回收将更频繁的发生,但GC每次作的事情也减小了,减少了进程的工做集。
CLR将大小为85000字节或更大的对象称为大对象,它不是在小对象的地址空间分配,而是在进程地址空间的其余地方分配。目前版本的GC不压缩大对象,由于在内存中移动它们代价太高。大对象老是第2代。因此只能为须要长时间存活的资源建立大对象。分配短期存活的大对象会致使第2代被更频繁的回收,损害性能。
垃圾收集器将托管堆中对象分为三代:0、1和2,在CLR初始化时,会选择为三代设置不一样的阙值容量,通常为:第0代大约为256KB,第1代2MB,第2代10MB。容量越大效率越低,而GC收集器会自动调节其阙值容量来提高执行效率。在CLR初始化后,首先添加到托管堆中的对象都被定位第0代对象,当有垃圾回收执行时,未被回收的对象代龄将提高一级,变成第1代对象,然后新建对象仍未第0代对象。代龄越小表示对象越新,一般状况下其生命周期也最短,所以GC老是先收集第0代的不可达对象内存。
随着对象的不断建立,垃圾收集再次启动时则只会检查0代对象并回收0代垃圾对象。而1代对象因为未达到1代容量阙值,则不会进行垃圾回收操做,从而有效地提升了垃圾收集的效率,而这也是代龄机制在垃圾回收中的性能优化做用。当第0代对象释放的内存不足以建立新的对象,同时1代对象的体积也超出了容量阙值是,垃圾收集器将同时对0代和1代对象进行垃圾回收。回收以后,未被回收的1代对象变化2级对象,未被回收的0代对象升级为1代对象,然后新建的对象仍为第0代对象。
注:微软强烈建议不要经过GC.Collect方法来强制执行垃圾收集,这样会妨碍GC自己的工做方式,经过Collect会使对象代龄不断提高,扰乱应用程序的内存使用。只有在明确知道有大量对象中止引用时,才考虑使用GC.Collect方法来调用收集器。
垃圾回收通常在下列状况下进行:
1 内存不足溢出时,更确切的应该说是第0代对象充满时。
2 调用GC.Collect方法强制执行垃圾回收。(通常不要执行此方法)
3 Windows报告内存不足时,CLR将强制执行垃圾回收。
4 CLR卸载AppDomain时,GC将对全部代龄的对象执行垃圾回收。
5 CLR正在关闭。CLR在进程正常终止时关闭。关闭期间,CLR任务进程中一切都不是根。对象有机会进行资源清理,但CLR不会试图压缩或释放内存。整个进程都要终止了,Windows将回收进程的所有内存。
6 其余状况,如物理内存不足,超出短时间存活代的内存段门限,运行主机拒绝分配内存等。
CLR启动时会选择一个GC模式,进程终止前该模式不会改变。两种模式:
工做站:
该模式针对客户端应用程序优化GC。GC形成的延时很低,应用程序线程挂起的时间很短,避免使用户感到焦虑。在该模式中,GC假定机器上运行的其余应用程序都不会消耗太多的CPU资源。
服务器:
该模式针对服务器端应用程序优化GC。被优化的主要是吞吐量和资源利用。GC假定当前应用程序是服务器上惟一的应用程序,该模式会致使托管堆被分隔为多个部分,每个CPU一份,而且这些部分是能够并行执行的。
默认状况下应用程序运行在工做站模式,而且支持同时收集(Asp.net和Sqlserver 默认采用服务器模式),若是服务器应用程序运行在单处理上,那么GC会采用工做站模式而且不支持同时收集。
GC还支持两种子模式:并发(默认)和非并发。在并发方式中,GC有一个额外的后台线程,在应用程序运行期间并发的标记对象,当由于分配对象形成第0代超出运算时,GC挂起全部线程,若是判断回收0代或1代,GC如常进行。若是须要回收第2代,GC会增大第0代的大小(超过其运算)。在应用程序运行期间,GC运行一个普通优先级的后台线程查找不可达对象,找到以后,GC再次挂起全部线程时,判断是否要压缩(移动)内存。如决定压缩,内存会进行压缩,根引用会被修正,而后应用程序恢复运行。这次GC花费时间比日常少,由于不可达对象集合已构造好。但GC也可能决定不压缩内存,实际上,GC更倾向于选择不压缩。可用内存多,GC不压缩,这样有利于加强性能,但会增大应用程序的工做集。使用并发GC,应用程序消耗的内存一般比使用非并发GC的多。
GC模式是针对进程进行配置的,进程运行期间不能更改。但应用程序可使用GCSettings类的GCLatencyMode属性对GC进行某种程度的控制。
LowLatency模式通常用它执行一次短时间的、时间敏感的操做,再讲模式设置成普通的Batch或Interactive。LowLatency期间,GC会全力避免回收任何第2代对象,由于那样花费的时间比较多。固然调用GC.Collect()或Windows告诉CLR系统内存低时,仍会回收第2代。该模式中,应用程序抛出OutOfMemoryException的概率大些,所以该模式的时间应尽可能短,避免分配太多对象,避免分配大对象。
GCLatencyMode oldMode = GCSettings.LatencyMode; System.Runtime.CompilerServices.RuntimeHelpers.PrepareConstrainedRegions(); try { GCSettings.LatencyMode = GCLatencyMode.LowLatency; //run code } finally { GCSettings.LatencyMode = oldMode; }
当包含本机资源的对象被GC时,GC会回收对象在托管堆上的内存,可是这会形成本机内存资源的泄露,是不容许的。若是须要清除非托管的包含本机资源的对象,CLR提供了终结机制,容许非托管对象在CLR断定为不可达、被断定为垃圾而且进行回收以前,执行终结机制,调用回收机制本身终结本身,释放本机资源。
终极基类System.Object定义一个虚拟方法finalize,容许对象在被CLR断定为垃圾时,调用对象的Finalize方法。
应用程序建立新对象,new操做符会从堆中分配内存。若是对象定义了Finalize方法,那么该类型的实例构造器在被调用以前,会将指向该对象的指针存放到一个终结列表finalization list中,终结列表是GC控制的一个内部的数据结构,列表中每一项都指向一个对象,回收该对象的内存前应调用它的Finalize方法。当对象进行GC时,若是GC认为终结对象是垃圾时,会将对象从终结列表中移出,同时移入到freachable队列中,freachable队列也是GC的一种内部数据结构,队列中的每一个引用都表明其Finalize方法已准备好调用的一个对象。一个特殊的高优先级的CLR线程专门调用Finalize方法,可避免潜在的线程同步问题,因为该线程的特殊性,finalize中不要访问线程的本地存储。
当一个对象不可达时,GC把它视为垃圾,该对象从终结列表移至freachable队列中,对象再也不认为是垃圾,不能回收它的内存。GC标记freachable对象是,将递归标记对象中的引用类型所引用的对象,因此这些对象也必须复活以便在回收过程当中存活,以后GC才结束垃圾标识,此过程当中,一些本来认为是垃圾的对象复活了,而后GC压缩移动可回收内存,将复活的对象提高到老的一代。如今,特殊的终结现场清空freachable队列,执行每一个对象的finalize方法。下一次对老一代进行垃圾回收时,会发现已终结的对象成为真正的垃圾,由于没有应用程序指向它们,freachable队列也不在指向它们。因此这些对象的内存会直接回收。整个过程,可终结对象须要执行两次垃圾回收才能是否占用的内存。在实际应用中,因为对象可能被提高至另外一代,因此可能不止进行两次垃圾回收。
对于非托管资源,须要开发者手动清理,方法主要有:Finalize方法和Dispose方法。
Finalize:
Finalize方法又称为终止化操做:经过对自定义类型实现一个Finalize方法来释放非托管资源,而终止化操做在对象的内存回收以前经过调用Finalize方法来释放资源。在析构函数中重写Finalize方法,当垃圾管理器启动时,对于断定为可回收的垃圾对象,GC会自动执行其Finalize方法清理非托管资源。
protected override void Finalize() { try { //执行自定义资源清理操做 } finally { base.Finalize(); } }
Finalize的缺点是:
终止化操做的时间没法控制,执行顺序也不能保证。
Finalize方法会极大的损失性能,GC使用一个终止话队列的内部结构来跟踪具备Finalize方法的对象。
重写finalize方法的类型对象,其引用类型对象的代龄将被提高,带来内存压力。
Dispose:
Dispose模式的实现是:定义的类型必须实现System.IDisposable接口,该接口中定义了一个公有无参数的Dispose方法,程序设计者能够在Dispose方法中实现对非托管资源的清理工做。
下面编写一个项目中遇到使用Dispose方法的例子,功能是在套接字使用完毕后释放资源
public class SocketConnection : IDisposable { //逻辑操做 //..................... //实现Dispose public void Dispose() { try { this.ClientSock.Shutdown(SocketShutdown.Both); this.ClientSock.Close(); this.Server = null; } catch (Exception ex) { } } }
总结:
在.net中,在堆栈上分配的资源在调用结束后,其内存自动会释放。
托管堆中的资源,由CLR的垃圾管理器进行清理操做。
对于非托管资源,必须由程序设计者进行操做,而对于Finalize和Dispose,最好采用Dispose方法。
Java1.2以前引用的定义:若是reference类型的数据中存储的数值表明的是另一块内存的起始地址,就称这块内存表明着一个引用。
Java1.2以后,强引用strong reference、软引用soft reference、弱引用weak reference、虚引用phantom reference。
强引用:相似Object obj = new Object(),只要强引用还存在,GC永远不会回收掉被引用的对象。
软引用:描述一些还有用但并不是必需的对象。对应软引用关联着的对象,在系统将要发生内存溢出异常以前,将会把这些对象列进回收范围之中进行第二次回收。若是此次回收尚未足够的内存,才会抛出内存溢出异常。
弱引用用来描述非必须的对象的,它的强度比软引用更弱一些,倍弱引用关联的对象只能生存到下次GC发生以前。
虚引用称为幽灵引用或者幻影引用,是最弱的一种引用关系。一个对象是否有虚引用的存在,彻底不会对其生存时间构成影响,也不发经过虚引用来取得一个对象实例,惟一目的是能在这个对象被GC时收到一个系统通知。
永久代的垃圾收集主要回收两部份内容:废弃常量和无用的类。
标记-清除算法Mark-Sweep:
问题有:效率问题,标记和清除的效率都不高。
空间问题,标记清除后会产生大量不连续的内存碎片,致使之后程序须要分配大对象时,没法找到足够连续的内存而不得不提起触发另外一次GC。
复制算法Copying:
将内存分为大小相等的两块,每次只使用 其中一块,用完时将存活的对象复制到另一块上,再把已使用的内存空间一次清理掉。代价是:内存缩小了原来的一半。现代商业的通常作法是分配一块较大的Eden空间和两块较小的Survivor空间。每次使用Eden和其中一块Survivor。HotSpot默认比例为8:1。
标记-整理算法:过程与标记-清除算法同样,但后续步骤不是直接对可回收对象进行整理,而是让全部存活对象都向一端移动。
分代收集算法Generational Collection:
将java堆分为新生代和老年代,根据各代的特色采用适当的收集算法。新生代中,每次都有大批对象死去,选用复制算法,只须要付出少许存活对象的复制成本就能够完成收集。老年代对象存活率高、没有额外空间对它进行分配担保,采用标记-清理或者标记-整理算法。
在讨论垃圾收集器的上下文语境中:
并行parallel:指多条垃圾收集线程并行工做,但此时用户线程仍然处于等待状态。
并发Concurrent:指用户线程与垃圾收集线程同时执行(但不必定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另外一个CPU上。
Serial收集器:
最基本、发展历史最悠久的收集器,它是一个单线程收集器。单线程的意义并不只仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工做,更重要的是在进行GC时,必须暂停其余全部的工做线程,直到它收集结束。它依然是虚拟机运行在client模式下的默认新生代收集器。简单而高效。
ParNew收集器:
其实就是Serial收集器的多线程版本。它是许多运行在server模式下的虚拟机中首选的新生代收集器,其中一个与性能无关的重要缘由是,除了serial收集器外,目前只有它能与CMS收集器配合工做。
Parallel Scavenge收集器:
特色是关注点不同,它关注点是达到一个可控制的吞吐量。吞吐量值cpu用于运行用户代码的时间与cpu总消耗时间的比值。停顿时间越短越适合须要与用户交互的程序,提高用户体验,而高吞吐量则能够高效率地利用cpu时间,尽快完成程序的运算任务,主要适合在后台运算而不须要太多交互的任务。
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。吞吐量优先。在注重吞吐量以及CPU资源敏感的场合,均可以优先考虑Parallel Scavenge+Parallel Old收集器。
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
整个过程分为4个步骤:
初始标记、并发标记、从新标记、并发清除,其中初始标记、从新标记这两个步骤仍须要“stop the world”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,从新标记阶段则是为了修正并发标记期间因用户程序继续运做而致使标记产生变更的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但远比并发标记的时间短。因为整个过程当中耗时最长的并发标记和并发清除过程收集器均可以与用户线程一块儿工做,因此整体来讲,CMS收集器的内存回收过程是与用户现场一块儿并发执行的。
CMS的缺点:
CMS收集器对CPU资源很是敏感。并发阶段,虽然不会致使用户的线程停顿,可是由于占用了一部分线程(或者说CPU资源)而致使应用程序变慢,总吞吐量会下降。
CMS收集器没法处理浮动垃圾,可能出现Concurrent Mode Failure失败而致使另外一次FULL GC的产生。
CMS收集器的标记-清除算法,会有大量空间碎片产生,空间碎片过多时,可能会给大对象分配带来很大的麻烦。
G1收集器
G1 Garbage-First收集器是一款面向服务端的垃圾收集器。
有以下特色:
并行与并发
分代收集
空间整合:总体来看是基于标记-整理,从局部来看是具备复制算法实现的。
可预测的停顿
使用G1收集器时,它将整个java堆划分为多个大小相等的独立区域,虽然还保留有新生代和老年代的概念,但已再也不是物理隔离的了,它们都是一部分Region的集合。之因此是可预测的,是由于它能够有计划的避免在整个java堆上进行全区域的垃圾收集,G1跟踪各个Region里面的垃圾堆积的价值大小,在后台维护一个优秀列表,每次根据容许的收集时间,优先回收价值最大的Region,这种使用Region划份内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内能够获取尽量高的收集效率。
G1收集器的步骤:
初始标记-并发标记-最终标记-筛选回收。
Java内存分配与回收策略:
对象优先在Eden分配
大对象直接进入老年代
长期存活的对象将进入老年代