Java垃圾回收策略

来源:互联网 发布:外国人眼中的丑女知乎 编辑:程序博客网 时间:2024/05/17 01:44

垃圾回收提供了内存管理的机制,使得应用程序不需要在关注内存如何释放,内存用完后,垃圾收集会进行收集,这样就减轻了因为人为的管理内存而造成的错误,比如在C++语言里,出现内存泄露时很常见的。Java语言是目前使用最多的依赖于垃圾收集器的语言,但是垃圾收集器策略从20世纪60年代就已经流行起来了,比如Smalltalk,Eiffel等编程语言也集成了垃圾收集器的机制。

     GC就是负责在对象“不可达”的时候将对象回收,常见的语句例如:Object o = null;
  而GC本身是如何工作的呢?当系统在创建对象的时候,即当我们使用new关键字创建一个对象的时候,GC就开始监控对象的地址、大小以及使用状态。一般情况下,Java的GC机制都有特定的回收算法,GC通常会使用有向图的方式来记录堆中的所有对象,通过此种方式确定哪些对象是“可达的”,而哪些是“不可达的”。当GC判断一些对象不可达的时候,GC就有责任回收相关内存空间。但是,因为平台的不同,往往在调用System.gc()的时候,存在太多不确定性,可以这样认为,当程序调用了System.gc()过后,仅仅是程序向JVM发送了请求,至于JVM究竟在什么时候进行垃圾回收,不同的平台不一样。

    GC在JVM中通常是启动了一个新的进程或者一组新的进程,它本身和Java用户程序一样需要占用heap空间,运行时也占用CPU。设计GC的时候,必须要在停顿时间和回收率之间进行权衡,原因在于它本身占用了Heap,如果GC运行时间太长,用户就会感觉到Java程序本身会有一定的停顿,如果运行时间太短,则有很多内存没有回收,使得程序里面创建的Java对象占用了大量的内存。

     回收区域?
    Java方法栈、本地方法栈以及PC计数器随方法或线程的结束而自然被回收,所以这些区域不需要考虑回收问题。Java堆和方法区是GC回收的重点区域,因为一个接口的多个实现类需要的内存不一样,一个方法的多个分支需要的内存可能也不一样,而这两个区域又对立于栈可能随时都会有对象不再被引用,因此这部分内存的分配和回收都是动态的。

    为什么分代?
    分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
    在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。
    试想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是他们依旧存在。因此,分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。

    如何分代?
    JVM中的共划分为三个代:年轻代(Young Generation)、年老点(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。如图所示:

     年轻代(Young Generation):
     所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分三个区。一个Eden区,两个Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制年老区“(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。
     年老代(Old Generation):
     在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
     持久代(Permanent Generation):
     用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=<N>进行设置。

    如何判定垃圾(对象已死)?
    所有的垃圾收集算法都面临同一个问题,那就是找出应用程序不可到达的内存块,将其释放,这里面得不可到达主要是指应用程序已经没有内存块的引用了,而在JAVA中,某个对象对应用程序是可到达的是指:这个对象被根(根主要是指类的静态变量,或者活跃在所有线程栈的对象的引用)引用或者对象被另一个可到达的对象引用。

  

    Reference Counting(引用计数)
    引用计数是最简单直接的一种方式,这种方式在每一个对象中增加一个引用的计数,这个计数代表当前程序有多少个引用引用了此对象,如果此对象的引用计数变为0,那么此对象就可以作为垃圾收集器的目标对象来收集。
    优点:简单,直接,不需要暂停整个应用
    缺点:1)需要编译器的配合,编译器要生成特殊的指令来进行引用计数的操作,比如每次将对象赋值给新的引用,或者者对象的引用超出了作用域等。2)不能处理循环引用的问题
    根搜索
    首先要暂停整个应用程序,然后开始从根对象扫描整个堆,通过选取一些根对象作为起始点,开始向下搜索,如果一个对象到根对象不可达时,则说明此对象已经没有被引用,是可以被回收的。可以作为根的对象有:栈中变量引用的对象,类静态属性引用的对象,常量引用的对象等。因为每个线程都有一个栈,所以我们需要选取多个根对象。
    另外,在这个搜索过程中的一些优化:
    1)如果每次扫描整个堆,那么势必让GC的时间变长,从而影响了应用本身的执行。因此在JVM里面采用了分代收集,在新生代收集的时候minor gc只需要扫描新生代,而不需要扫描老生代。
    2)JVM采用了分代收集以后,minor gc只扫描新生代,但是minor gc怎么判断是否有老生代的对象引用了新生代的对象,JVM采用了卡片标记的策略,卡片标记将老生代分成了一块一块的,划分以后的每一个块就叫做一个卡片,JVM采用卡表维护了每一个块的状态,当JAVA程序运行的时候,如果发现老生代对象引用或者释放了新生代对象的引用,那么就JVM就将卡表的状态设置为脏状态,这样每次minor gc的时候就会只扫描被标记为脏状态的卡片,而不需要扫描整个堆。如下图:

    垃圾回收策略?
    Mark-Sweep Collector(标记-清除收集器)
    标记清除收集器最早由Lisp的发明人于1960年提出,标记清除收集器停止所有的工作,从根扫描每个活跃的对象,然后标记扫描过的对象,标记完成以后,清除那些没有被标记的对象。如下图:


    优点:1)解决循环引用的问题;2)不需要编译器的配合,从而就不执行额外的指令
    缺点:1)每个活跃的对象都要进行扫描,收集暂停的时间比较长。2)回收后内存碎片很多,如果之后程序运行时申请大内存,可能会又导致一次GC
    这种策略却是后两种策略的基础。正因为它的缺点,所以促成了后两种策略的产生。
    Copying Collector(复制收集器)
    将内存分为两块,标记完成开始回收时,将一块内存中保留的对象全部复制到另一块空闲内存中。实现起来也很简单,当大部分对象都被回收时这种策略也很高效。为提高效率,可以将堆不按1:1的比例分离,而是按8:1:1分成一块Eden和两小块Survivor区,每次将Eden和Survivor中存活的对象,复制到另一块空闲的Survivor中。这三块区域并不是堆的全部,而是构成了新生代。新生代中的每次GC都会清除掉大部分对象。复制收集器具体过程可以参考下图:

    优点:1)只扫描可以到达的对象,不需要扫描所有的对象,从而减少了应用暂停的时间
    缺点:1)需要额外的空间消耗,某一个时刻,总是有一块内存(一个survivor区)处于未使用状态;2)复制对象需要一定的开销
    Mark-Compact Collector(标记-整理收集器)
    标记整理收集器汲取了标记清除和复制收集器的优点,它分两个阶段执行,在第一个阶段,首先扫描所有活跃的对象,并标记所有活跃的对象,第二个阶段首先清除未标记的对象,然后将活跃的的对象复制到堆得底部。标记整理收集器的过程示意图请参考下图:Mark-compact策略极大的减少了内存碎片,并且不需要像Copy Collector一样需要两倍的空间。如下图:

    JVM的垃圾回收器
    GC的执行时要耗费一定的CPU资源和时间的,因此在JDK1.2以后,JVM引入了分代收集的策略,其中对新生代采用"Mark-Compact"策略,而对老生代采用了“Mark-Sweep"的策略。其中新生代的垃圾收集器命名为“minor gc”,老生代的GC命名为"Full Gc 或者Major GC".其中用System.gc()强制执行的是Full GC。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行判断。

    Serial Collector Serial Collector是指任何时刻都只有一个线程进行垃圾收集,这种策略有一个名字“stop the whole world",它需要停止整个应用的执行。这种类型的收集器适合于单CPU的机器。
    Serial Copying Collector
    此种GC用-XX:UseSerialGC选项配置,它只用于新生代对象的收集。1.5.0以后。-XX:MaxTenuringThreshold来设置对象复制的次数。当eden空间不够的时候,GC会将eden的活跃对象和一个名叫From survivor空间中尚不够资格放入Old代的对象复制到另外一个名字叫To Survivor的空间。而此参数就是用来说明到底From survivor中的哪些对象不够资格,假如这个参数设置为31,那么也就是说只有对象复制31次以后才算是有资格的对象。这里需要注意几个个问题:
    From Survivor和To survivor的角色是不断的变化的,同一时间只有一块空间处于使用状态,这个空间就叫做From Survivor区,当复制一次后角色就发生了变化。
    如果复制的过程中发现To survivor空间已经满了,那么就直接复制到old generation。比较大的对象也会直接复制到Old generation,在开发中,我们应该尽量避免这种情况的发生。
    Serial Mark-Compact Collector
    串行的标记-整理收集器是JDK5 update6之前默认的老生代的垃圾收集器,此收集使得内存碎片最少化,但是它需要暂停的时间比较长。

    Parallel Collector Parallel Collector主要是为了应对多CPU,大数据量的环境。Parallel Collector又可以分为以下两种:
    Parallel Copying Collector
    此种GC用-XX:UseParNewGC参数配置,它主要用于新生代的收集,此GC可以配合CMS一起使用。1.4.1以后Parallel Mark-Compact Collector,此种GC用-XX:UseParallelOldGC参数配置,此GC主要用于老生代对象的收集。
    Parallel scavenging Collector
    此种GC用-XX:UseParallelGC参数配置,它是对新生代对象的垃圾收集器,但是它不能和CMS配合使用,它适合于比较大新生代的情况,此收集器起始于jdk 1.4.0。它比较适合于对吞吐量高于暂停时间的场合,Serial gc和Parallel gc可以用如下的图来表示:

   Concurrent Collector
   Concurrent Collector通过并行的方式进行垃圾收集,这样就减少了垃圾收集器收集一次的时间,这种GC在实时性要求高于吞吐量的时候比较有用。此种GC可以用参数-XX:UseConcMarkSweepGC配置,此GC主要用于老生代和Perm代的收集。

   何时进行垃圾回收?

   由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC(Minor GC)和Full GC。
   Scavenge GC
   一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。
   Full GC
   对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC: 年老代(Tenured)被写满;持久代(Perm)被写满;System.gc()被显示调用;上一次GC之后Heap的各域分配策略动态变化

原创粉丝点击