Java Garbage Collection(GC)介绍

Garbage Collection 译为垃圾收集器(以下简称GC),主要负责内存分配、确保所有被引用的对象保留在内存中、将那些无法达到的对象引用所占用的内存回收。被引用的对象通常被称为活动的(live)。不再被引用的对象,认为是已经消亡,被称之为垃圾。寻找与释放对象占用空间的过程被称为垃圾收集/回收。


        内存作为系统中重要的资源,对于系统稳定运行和高效运行起到了关键的作用,Java 和C 之类的语言不同,不需要开发人员来分配内存和回收内存,而是由JVM 来管理对象内存的分配以及对象内存的回收(GC)。 
        GC 其实是一种动态存储管理技术。主要是按照特定的垃圾收集算法(Garbage Collection Algorithm 简称GC算法)来实现自动资源回收的功能。简单地讲,就是将那些程序中显式释放内存的代码交由GC 在后台自动完成,能提供这种功能的编程语言,我们就说它支持GC,比如典型的Java、C#、Python 等,本文主要讨论HotSpot 系列Java 虚拟机中的GC。


        高级编程语言采用GC 模式,省去了程序员自己管理内存的麻烦和危险。操作内存空间其实是极其危险的,稍有不慎可能导致内存泄漏(memory leak)或者悬空指针(dangling pointer)甚至整个系统崩溃。因此GC 的使用也一定程度上提升了系统的安全与稳定。


        下图就描述了一个理想中系统模型,图中随着处理器的增加,GC花费时间不变的情况下所损失的吞吐量(可以简单的理解程序实际工作时间)。 


  
        当只花费1%时间去处理垃圾时,在32个处理器环境下最终损失了20%多的吞吐量,也就是说程序处理能力下降了20%多。 
        随着在GC 上花费时间越来越多,程序损失的吞吐量也在呈抛物线迅速上涨。当花费30%时间在GC 上时,程序几乎已经瘫痪,已经失去了运行的意义。 
        由此可见,在开发小型程序时我们往往可以不去关注GC 方面的优化及处理,但一旦系统规模庞大后,你就再也无法忽视这方面性能上的差异,几处微不足道的改良可能会获得性能大幅提升,所以无论在任何系统开发中都应该着重关注GC 方面的问题。


        GC 的设计选择: 
        (1)串行回收(Serial)VS并行回收(Parallel) 
        串行就是不管有多少个CPU,始终只有一个CPU用来执行回收操作,而并行就是把整个回收工作拆分成多个,由多个CPU同时执行。并行回收执行会快,但复杂度增加,另外也有其他一些副作用,比如内存碎片会增加。 
        (2)并发执行(Concurrent)VS应用程序停止(Stop-the-world) 
        Stop-the-world的GC方式在执行GC的同时会导致应用程序的暂停。并发执行的GC虽然不会导致应用程序的暂停,但由于并发执行GC要解决和应用程序的执行冲突(应用程序可能会在GC的过称中修改对象),并发执行GC执行的消耗会高于Stop-the-world,而且执行也需要更多的内存堆。 
        (3)压缩(Compacting)VS不压缩(Non-compacting)VS拷贝(Copying) 
        为了减少内存碎片,支持压缩的GC会把所有的活对象搬迁到一起,然后将之前占用的内存全部回收。不压缩式的GC顾名思义就是在GC的过程中不压缩内存,较之压缩式的GC,不压缩式的GC回收内存快了,而分配内存慢了,而且无法解决内存碎片的问题。拷贝式的GC会将活对象拷贝到不同的内存区域中,这种方式的优点是源数据可以被认为已经清空并可以用来分配,缺点也很明显,需要拷贝数据和额外的内存。


        GC 几种重要的性能指标: 
        (1)Throughput: 经过长期运行后除去GC 处理时间系统实际执行时间所占全部时间的百分比。 
        (2)Garbage collection overhead: GC 开销,吞吐量的倒数,也就是说,在垃圾收集上花费的总时间的百分比。 
        (3)Pause time: 程序执行期间暂停的时间,程序暂停后GC 开始工作,即前面说的stop-the-world 时间。 
         (4)Frequency of collection: 相对于程序执行垃圾收集发生的频率。 
         (5)Footprint: 一些参数的大小,例如堆大小(Heap Size)。 
        (6)Promptness: 对象变为垃圾后到该内存被清理的时间。


        此时我们还需要了解一些其他事情。 
        (1)默认的本文以Hotspot JVM(版本1.5以上) 类型虚拟机为基础讨论。 
        (2)在J2SE1.4版本之前,JVM 是不支持并行GC的。 
        (3)名词"Stop-the-world",Stop-the-world 会在任何一种GC算法中发生。Stop-the-world 意味着JVM 因为要执行GC而停止了应用程序的执行。当Stop-the-world发生时,除了GC所需的线程以外,所有线程都处于等待状态,直到GC任务完成。GC优化很多时候就是指减少Stop-the-world发生的时间。


下面是memory management whitepaper的原文:
When stop-the-world garbage collection is performed, execution of the application is completely suspended during the collection.
  
        (4)默认本文讨论GC 内存收集区域为Heap,方法区暂不考虑。


        默认的Hotspot 系列虚拟机采用分代方式对Heap 内存进行管理与垃圾清除,Heap Memory 被分为两大区域: 
        - Young/New Generation 新生代 
        新生对象放置在新生代中,新生代由Eden 与Survivor Space 组成。 
        - Old/Tenured Generation 老年代 
        老年代用于存放程序中经过几次垃圾回收后还活动的对象


        (1)Young/New Generation 新生代 
        程序中新建的对象都将分配到新生代中,新生代又由Eden(伊甸园)与两块Survivor(幸存者) Space 构成。Eden 与Survivor Space 的空间大小比例默认为8:1,即当Young/New Generation 区域的空间大小总数为10M 时,Eden 的空间大小为8M,两块Survivor Space 则各分配1M,这个比例可以通过-XX:SurvivorRatio 参数来修改。Young/New Generation的大小则可以通过-Xmn参数来指定。 
  
        Eden: 刚刚新建的对象将会被放置到Eden 中,这个名称寓意着对象们可以在其中快乐自由的生活。 
        Survivor Space: 幸存者区域是新生代与老年代的缓冲区域,两块幸存者区域分别为s0 与s1,当触发Minor GC 后将仍然活动的对象移动到S0中去(From Eden To s0)。这样Eden 就被清空可以分配给新的对象。 
        当再一次触发Minor GC后,S0和Eden 中活动的对象被移动到S1中(From s0To s1),S0即被清空。在同一时刻, 只有Eden和一个Survivor Space同时被操作。所以s0与s1两块Survivor 区同时会至少有一个为空闲的,这点从下面的图中可以看出。 
        当每次对象从Eden 复制到Survivor Space 或者从Survivor Space 之间复制,计数器会自动增加其值。 默认情况下如果复制发生超过16次,JVM 就会停止复制并把他们移到老年代中去。如果一个对象不能在Eden中被创建,它会直接被创建在老年代中。


        (2)Old/Tenured Generation 老年代 
        老年代用于存放程序中经过几次垃圾回收后还活动的对象,例如缓存的对象等,老年代所占用的内存大小即为-Xmx 与-Xmn 两个参数之差。


        当新生代内存被占满后,新生代的收集器(一般为'minor collection')就会对新生代进行垃圾收集执行工作。当老年代或永久代被占满后,通常是full collection(或被称为major collection)就会执行垃圾清理。这样所有分代内存都会被清理干净。 
        一般情况下,新生代会被专门的收集算法首先清理。这种算法是专为新生代所设计的,所以这种识别新生代中垃圾的算法通常是最有效的。类似的老年代也有专门的垃圾清理算法支持。


        在Hotspot JVM 中的几种GC 类型:


        1.Serial Collector 
        串行收集,在同一时间只会执行一件垃圾清理任务。例如,即使当多个CPU 是可用的,但却只有一个CPU 是用于执行垃圾收集。也就是说在串行收集器下,所有任务将按顺序执行。


        新生代中串行收集器如何工作 
        Eden 中还活动的对象会被拷贝至初始为空的survivor space,在图中被标为'To'空间。被拷贝对象中不包括那些因为过大,'To'无法容纳的对象,而这样的对象会被直接复制到老年代。 
        如果另一块survivor space(图中标为From)中存在相对比较年轻(young)并且仍活动的对象,则会被一同复制到另一块'To' 中(即从From 复制到To)。 
        复制开始前(红色部分为垃圾):




 


        如果此时To 空间已满,那些Eden 里仍旧活动或尚未复制到From 空间的里的对象,将一直活动下去,不论经过多少次新生代垃圾收集。也就是说既然你无法给我提供容身之处,那么我将肆无忌惮的逍遥下去。 
        在活动对象复制完成之后,Eden 或From 空间中的剩余对象均会被定义为not live,他们不再需要进行检查。这些对象已经不会再被我们使用,也就是我们所说的“垃圾”,这些垃圾会在下次垃圾清理后消失。 
        清理及复制完成后:




  
        老年代中串行收集器如何工作 
        在老年代与永久代中,串行垃圾收集器使用mark-sweep-compact 收集算法。mark-sweep-compact 算法有3个阶段:mark(标记阶段),sweep(扫描阶段),compact(压缩阶段)。 
        标记阶段: 收集器首先确认哪些对象还活动着。 
        扫描阶段: 扫描整个老年代并标记垃圾。 
        压缩阶段: 扫描完成之后收集器执行移动压缩(sliding compaction),将live对象移动到老年代内存空间的起始部分(永生代中情况类似),这样在老年代内存空间的尾部会产生一个大的连续空间,方便在给新对象分配空间的时候执行bump-the-pointer。 
        下图为经过垃圾收集及压缩过后的老年代内存情况:




  
        串行收集器被大多数客户端程序所选择,因为这样的程序并不会对暂停时间有严格要求。 
        从J2SE 5.0发行版后,串行收集器在那些不是服务级别的机器上被自动选为默认的垃圾收集器。可以声明-xx:+UseSerialGC 选项使用串行收集器。


        总结 
        串行收集器的优点:简单易用,非常容易管理。 
        串行收集器的缺点:效率不高,无法处理较大规模垃圾清理情况,更适用于客户端程序。


        2.Parallel Collector 
        并行收集器,也被称为吞吐量收集器,当使用并行收集器时,原本的垃圾收集任务会被分割成不同的子任务,并且这些子任务同时会分配在不同CPU 下执行。这样同时操作可以做到更加快速的垃圾清理,但也会消耗更多的系统资源和产生清理不完全(潜在碎片)的情况。


        在新生代中,并行收集器使用并行版本的串行收集器收集算法,所以新生代的并行回收算法和串行收集器是一样的,只是增加了并行的能力,新生代的并行收集器仍然是stop-the-world 和coping收集器,但通过在多个CPU 中并发运行,降低了GC 的开销并提升了应用程序的吞吐量。


        原本年轻代中的复制任务在并行收集器中会被分为多个任务,例如:




 


        假设任务1执行时间1秒,任务2执行时间2秒,任务3执行时间3秒。 
        在串行收集器中三个任务将被顺序执行,例如:1-->2--3,执行时间为三者花费时间之和则为6秒。 
        但在并行收集器中利用多CPU优势,三个任务将会同时进行,所以执行时间为最长任务执行时间则为3秒。并行收集器大大的缩短了垃圾收集过程的执行时间,下图为两种收集器的对比:




 


        老年代的并行回收使用的也是串行的mark-sweep-compact回收算法,特别注意的是并行收集器对老年代的回收并没有并行处理的能力,也就是说并行收集器只对新生代并行回收。


        并行处理器可以在多CPU 硬件支持下更迅速的执行垃圾清理,从而缩短程序暂停时间,可以声明-xx:+UseParallelGC 选项使用并行收集器。


        总结 
        并行收集器的优点:充分利用硬件资源,垃圾清理工作较串行垃圾收集器更为迅速,效率更高。 
        并行收集器的缺点:硬件资源消耗更多,老年代下并行收集器与串行收集器相同,在某种程度上存在垃圾清理不完全的情况。


        3.Parallel Compacting Collector 
        并行压缩收集器是在J2SE1.5后引入,与并行收集器最大的不同是对老年代的回收使用了不同的算法,并行压缩收集器最终会取代并行收集器。


        新生代中使用并行压缩回收 
        并行压缩收集器对新生代的回收算法跟并行收集器相同。


        老年代中使用并行压缩回收 
        并行压缩收集器同样还是会引起stop-the-world 效应,并行主要是体现在sliding compaction 上。收集器使用了3个阶段: 
        首先, 整个代在逻辑上都分配成几个固定大小的region(区域)。 
        然后, 进入Marking 阶段,在应用程序中可以直接引用到的活动对象被多个GC 线程所划分掉,然后所有的活动对象就可以以并行地方式来实现对它们的标注。当某个Object 被鉴定为活动对象的时候,就会更新这个对象所在区块的大小以及该对象位置的信息。marking 结束之后是summary 阶段,summary 阶段操作的是region 而不是对象了。由于每次的GC 的压缩都会每一个代的左边的部分区域活动对象密度特别高,保存了多数活动对象。对这样的高密度活动对象的区域进行压缩往往不划算。所以在summary 阶段会从最左边的区域开始检验每个区域的密度,当进行到某个区域中能回收的空间达到了某个数值的时候,那么收集器会判定该区域以及该区域右边的那些区域都是值得进行回收的。该区域左边的区域都会被标识成密集,不会有对象移动到这些密集区域去,而该区域和右边的区域之后都会被进行压缩,回收空间的操作。在summary 阶段会计算和保存每个活动对象在每个压缩区域的第一个字节的新位置。summary 阶段目前还是串行操作,虽然并行是可以实现的,但重要性不如对marking 和压缩阶段的并行来的重要。 
        最后, 压缩阶段,收集器利用summary 阶段生成的数据识别出有哪些区域是需要装填的,每个GC 线程就可以独立的将数据拷贝到这些区域中。这个过程就会heap 在一端很密集而另一端存在大块的空闲块。


并行压缩收集器适合运行在多个CPU的机器上,而且较之并行收集器增强了对老年代的并行回收,减少了系统停顿的时间,可以声明-xx:+UseParallelOldGC 选项使用并行压缩收集器。


        总结 
        并行压缩收集器的优点:充分利用硬件资源,较之并行收集器增强了对老年代的并行回收,收集器更为迅速,效率更高。 
        并行压缩收集器的缺点:硬件资源消耗更多,在某种程度上存在垃圾清理不完全的情况。


        4.Concurrent Mark-Sweep (CMS) Collector 
        在很多应用中,更加注重快速的相应时间而不是end-to-end 吞吐量(对于end-to-end 概念我也无法解释的很正确,希望了解的朋友补充)。新生代的垃圾回收通常不会造成长时间的应用程序中断,但是,对于老年代,特别是当Heap 已使用量比较大的时候会导致长时间的程序中断(虽然这种情况不常发生)。Hotspot JVM 引入CMS 的目的就是为了解决这个问题。


        新生代中使用CMS 
        收集方式与并行收集器一致。


        老年代中使用CMS 
        CMS 对老年代的回收多数是并发操作。垃圾收集循环开始的时候需要一个短暂的暂停,称之为初始标记(initial mark),这个阶段会识别出那些直接被引用的活动对象。然后进入了并发标记阶段(concurrent marking phase),收集器会从集合中标记所有可达的活动对象。 
        与此同时应用程序也在运行,那就无法保证所有的活动对象都会被识别、标记出来。于是应用程序会再次被暂停,在这个再标记(remark)阶段,收集器会访问在并发标记阶段中被修改过的对象并完成标记。由于再标记阶段较之初始标记更重要,所以会并发运行多个线程来提升效率。 
        在完成了再标识以后,Heap 中所有的活动对象都已经被标记出来,所以后续并发扫描阶段需回收的所有垃圾就已经被确认,接下来就可以运行并发清理了。




  
        因为一些任务,如在再标记阶段重访对象,将增加收集器的工作量,同时开销也会增加。所以对于大多数收集器而言试图减少停顿时间的副作用。 
        CMS 收集器是一个无压缩的收集器。也就是说那些被释放的空间不会像并行压缩收集器那样自动整理,使Heap 尾端留有大片空闲内存空间,如图所示:




  
        而且由于空闲空间是不连写的,收集器就必须要保存一份可用空间的列表。当需要分配对象的时候,收集器就要通过这份列表找到足够容纳新对象的空间位置,这就导致内存分配效率会比bump-the-pointer 算法差。由于老年代的分配效率下降将直接影响到新生代回收过程中转移至老年代的效率。


        另外,CMS 较之前的几种收集器需要更大的Heap,原因是在标记过程中,应用程序同时在运行,同时在分配对象,因此老年代也同时在增长。此外,虽然活动对象在标记阶段都会被识别出来,但有些在标记阶段成为垃圾的对象并不能同时被回收,只能等到下次垃圾收集的时候才能被回收。


        最后,由于没有压缩,所以就容易出现内存碎片。为了解决这个问题,CMS 会分析popular 对象的大小来预估下一步可能的需求,然后可能会对空闲的内存块分割或合并。CMS 不会等到老年代满的时候才运行内存回收,而是提前清理出内存空间。所以CMS 会利用之前内存回收的统计数据(收集所需要的时间、老年代被占满的时间等等),然后选择一个合适的开始回收时间。CMS 在老年代的内存占用率到达某个阀值的时候也会进行主动回收。这个阀值可以通过–XX:CMSInitiatingOccupancyFraction=n 参数来定义,缺省值是68,即68%。


        总之,CMS较之并发收集器以某些时候稍微增加新生代的回收时间、增加Heap 的使用量、减少一些吞吐量为代价减少了老年代回收过程中的停顿时间,而且CMS 会要求在应用程序运行过和收集器分享处理器资源。对那些会产生比较大老年代的应用程序而言,如果运行在多处理器上,CMS 是一个不错的选择。可以声明-xx:+UseConcMarkSweepGC 选项使用CMS。如果你还想让CMS 运行与增量模式下,则可声明–XX:+CMSIncrementalMode 选项启用增量模式。增量模式指的是把收集器的工作分成多个时间块,然后在两次新生代的回收期间加以运行,这种方式可以更进一步减少暂停的时间。


        总结 
        CMS 收集器的优点:对于老年代使用率比较高的应用程序适合CMS 收集器,对停顿时间有较严格要求的程序也比较适合使用CMS 收集器。 
        CMS 收集器的缺点:CMS 的缺点也比较明显,CMS会牺牲更多的硬件资源、吞吐量及Heap使用量等,所以在Java1.7之后引入了G1 收集器来全面替代CMS。


        5.Garbage-first(G1,Java1.7正式引入)


        Garbage-first Garbage Collector,简称G1 GC,是最终将用于代替Concurrent Mark-Sweep Garbage Collector(CMS GC)的新一代垃圾收集器。目前JDK1.6 update 14及以后版本的JVM 中已经继承了G1 GC,可以使用参数-XX:+UnlockExperimentalVMOptions -XX:+UseG1GC来启用。


        G1 是一个适用于服务器端、大内存、多CPU情景的垃圾收集器,主要目标是在维持高效率回收(high thoughput)的同时,提供软实时中断特性。用户可以指定一个时间上限,如果垃圾回收导致的程序暂停超过了用户设定的时间上限,会打断垃圾回收,恢复程序的执行。




 


        G1 的原理在于将堆划分成等一系列大小的区域,每一个区域都有一个对应的remembered set 结构,用来记录指向这个区域中的地址的其他区域的指针。 在垃圾回收时,选择记录最少的一个区域进行,按找这种方式选择出来的区域,通常是有用数据最少、垃圾最多的区域,这也就是“Garbage-first”名称的由来。假如没有外部的指针指向这个区域,就可以直接回收整块区域而不用进行内存Copy,这种情况真是太好了。 
        G1 收集器我会专门拿出一节来介绍。


        最后是对于GC 类型的使用情况分类:
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值