JVM垃圾回收模型

一.GC算法

1.1标记清除法(mark-sweep)

        顾名思义,标记清除法分为两个阶段:标记和清除。概括来说就是该算法首先标记处所需要清除的对象,然后将标记的对象进行清除。

        内容:首先从gcroot开始寻找存活的对象,将所有标记为要清除的对象进行回收,未被标记的对象仍然保存,被标记的对象被清理之后,剩余的对象并不会被jvm进行整理,而是保存在原地:这样就会出现一个显而易见的缺点,在多次GC之后,对象在jvm中就会零星占用很多空间,内存碎片堆积,导致内存的利用率低,并且大对象在加载到内存的时候出现问题;

        问题:效率不高,需要扫描所有的对象,堆越大,GC越慢,并且存在严重的内存碎片问题,空间碎片太多可能会导致后续使用中无法找到足够的连续内存而提前触发另一次的垃圾搜集动作,GC次数越多,碎片越严重。

 (图中红色代表已经标记为需要GC的对象,绿色代表不需要清理的对象)

1.2标记整理算法(Mark-Compact)

        标记过程同标记清理算法,只不过没有清理过程,而是将所有存活的对象向某一端移动,使零星的内存碎片整理成一片内存区域,在整理完成后,清除掉区域外的内存。

        优势:跟标记清理算法相比,解决了内存碎片的问题,内存的利用率提高。

1.3复制收集算法(Coping)

        1.3.1原始的复制收集算法

        首先将内存区域分成两块,每次都只利用其中的一块(A),将其中的一块区域(A)利用完后,将仍然存活的对象复制到另外一块内存区域(B)中,然后将整个内存区域(A)进行清理。

        问题:该算法的思想跟标记整理算法差不多,但是内存分配简单,每次清理和复制只需要移动固定的堆顶指针,按照内存顺序分配内存就可,问题也很明显,内存只利用了一半,内存的利用率较低。

        1.3.2现在的复制收集算法

        现在的虚拟机将内存分为4个部分:eden,from survivor,to survivor ,old(tentired  现在常用old)。

         每次只是用eden和其中一块survivor空间,当回收时将eden和survivor空间中还存活的对象一次性拷贝到另一个survivor空间上,然后清理用过的eden和survivor空间,oracle hotspot虚拟机默认eden 和 survivor的比例是 8:1 ,也就是每次只有百分之十的内存被浪费。

        好处:1.只需要扫描存活的对象,效率更高;2.不会产生碎片 3.复制算法非常适合对象存活时间比较短的对象,因为每次GC总能回收大部分的对象,复制的开销比较小。根据IBM的专门研究,98%的Java对象只会存活1个GC周期,对这些对象很适合用复制算法。而且不用1:1的划分工作区和复制区的空间 。

        问题:复制收集算法在对象存活率高得时候效率有所下降,就需要有额外的空间进行分配担保用于应付内存中所有对象都百分之百存活的极端情况(在新生代中可以使用老年代进行空间分配担保),所以在老年代不能直接采用这种算法,因为老年代的对象存活时间都相对较长。

1.4 分代算法(Generational)

                

         当前商业虚拟机的垃圾收集都是采用“分代收集”( Generational Collecting)算法根据对象不同的存活周期将内存划分为几块;一般是把Java堆分作新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法:

        1. 譬如新生代每次GC都有大批对象死去,只有少量存活,那就选用复制算法只需要付出少量存活对象的复制成本就可以完成收集;

        2. 有老年代作为空间分配担保;老年代采用Mark- Sweep或者Mark- Compact算法。     

        年轻代( Young Generation): 新生成的对象都放在新生代。

        年轻代用复制算法进行GC(理论上年轻代对象的生命周期非常短,所以适合复制算法,因为大部分都是不存活的对象),年轻代分三个区,一个Eden区,两个 Survivor区(可以通过参数设置 Survivor个数,在hotspot中就是我们常说的s0,s1)。对象在Eden区中生成,在新生代垃圾回收时,Eden区和From Survivor区中还存活的对象将被复制到另一个 Survivor区(称为To Survivor区),此次垃圾回收完成之后From Survivor和To Survivor区交换角色。下一次垃圾回收时重复上述过程,直到To Survivor 区被填满,然后一次性将To Survivor中的所有对象移动到老年代中。2个 Survivor是完全对称,轮流替换。Eden和2个 Survivor的缺省比例是8:1:1,也就是10%的空间会被浪费。可以根据 GC log的信息调整大小的比例。

        老年代(Old Generation):

        存放了经过一次或多次GC还存活的对象般采用Mark- Sweep或者Mark- Compact算法,进行GC有多种垃圾收集器可以选择。每种垃圾收集器可以看作一个GC算法的具体实现。可以根据具体应用的需求选用合适的垃圾收集器(追求吞吐量?追求最短的响应时间?)

   

二.垃圾回收器

先介绍一下在hotspot中GC的种类:

  • Scavenge GC (Minor GC):对新生代,触发时机是在新对象生成时,Eden空间满了,理论上Eden区大多数对象会在 Scavenge GC回收,复制算法的执行效率会很高, Scavenge GC时间比较短。

  • Full GC:对整个JVM进行整理,包括 Young、Old和Perm(永久代,jdk8没有,jdk为元空间),主要的触发时机:1)Old满了2)Perm满了3) system.gc() Full GC的执行效率很低,尽量减少 Full GC。

        分代模型是GC的宏观愿景,垃圾回收器是GC的具体实现,hotspot jvm提供多种垃圾回收器,我们需要根据具体的应用采用多种垃圾回收器

        垃圾回收器的并行(Parallel)和并发(Concurrent),并行指的是多个收集器的线程同时工作,但是用户线程处于等待状态;并发指的是收集器在工作的同时,可以允许用户线程工作,但是并发并不代表解决了GC的停顿问题,在关键步骤该停顿的还是要停顿,比如在收集器标记垃圾的时候,但是在清除垃圾的时候用户线程可以和GC线程并发执行。

2.1 serial 收集器

        串行serial收集器是最早的收集器,单线程收集器,Hotspot Client模式缺省的收集器,收集时会暂停所有工作线程(Stop The World,简称STW),因为是单线程GC,没有多线程切换的额外开销,简单实用

        New和 Old Generation都可以使用。在新生代,采用复制收集算法,在老年代,采用标记整理算法;

 

2.2 Serial Old 收集器

        Serial Old是单线程收集器,使用标记一整理算法, 是老年代的收集器

2.3 parnew收集器

  • Parnew收集器就是Serial收集器在新生代的多线程版本(Parallel New),是Server模式下新生代的缺省收集器,除了使用多个收集线程外,其余行为包括算法、STW、对象分配规则、回收策略等都与 Serial收集器一模一样。

  • 使用复制算法(因为针对新生代,效率比较高);

  • 只有在多CPU的环境下,效率才会比 Serial收集器高;

  • 可以通过-XX: Parallelg Cthreads来控制GC线程数的多少,需要结合具体CPU的个数。

2.4 Parallel Scavenge收集器

        Parallel Scavenge(并行清除) 收集器也是一个多线程收集器(Parallel就是并行的意思),也是使用复制算法,但它的对象分配规则与回收策略都与 Parnew收集器有所不同,它是以吞吐量最大化(即GC时间占总运行时间最小)为目标的收集器实现,它允许较长时间的STW换取总吞吐量最大化,jvm1.8默认在新生代使用Parallel Scavenge ,老年代使用Parallel Old收集。

2.5 Parallel Old 收集器

        JVM1.6提供,在此之前,新生代使用PS收集器的话,老年代除了使用Serial Old外别无选择,因为PS无法和CMS配合工作。

        jvm1.8默认在新生代使用Parallel Scavenge ,老年代使用Parallel Old收集。

        特点:

  • 配合新生代Parallel Scavenge在老年代的实现;

  • 采用多线程,Mark-Compact算法;

  • 更注重吞吐量Parallel Scavenge+ Parallel Old = 高吞吐量,但GC停顿可能不理想

 这两种收集器配合使用可以实现新生代和老年代都并行垃圾收集。

2.6 CMS收集器

        CMS(Concurrent Mark Sweep:并行标记清除收集器)是一种以最短停顿时间为目标的老年代收集器,使用CMS并不能达到GC效率最高(总的GC时间最小),但是它能尽可能降低服务的停顿时间

  • 只针对老年代,一般在新生代结合Parnew使用

  • CMS收集器使用的是标记--清除算法

  • 使用-XX:+ UseConcMarkSweepGC打开

收集步骤:方法一

CMS是基于“标记--清除”算法实现的,在老年代中的整个过程分为4个步骤:

        其中:初始标记,重新标记这两个步骤任然需要“stop the world”,其它两个步骤中用户线程是一起并发执行的

  • 初始标记(CMS initial mark),初始标记只是标记一下GC ROOTS 能直接关联到的对象,速度很快(stw)

  • 并发标记(CMS concurrent mark),并发标记阶段就是进行GC ROOTS Tracing 的过程,此时用户线程也是在同步执行的

  • 重新标记(CMS remark),重新标记阶段则是为了修正并发标记期间因为用户程序继续运作而导致标记产生变动的那一部分对象的标记记录( 这部分对象是指从 GC Roots 不可达的对象,因为用户程序的并发运行,又可达了),这个阶段的停顿时间一般会比初始标记阶段稍长一些,但是远比并发标记的时间短。

  • 并发清除(CMS concurrent sweep),收集在标记阶段被标识为不可访问的对象。The collection of a dead object adds the space for the object to a free list for later allocation. Coalescing of dead objects may occur at this point. Note that live objects are not moved.死亡对象收集为空闲列表增加了更多的空间,以便以后分配。在这一点上可能会发生死物体空间的的合并。请注意,不会移动活动对象。

        CMS收集器的运作步骤如下图所示,在整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以和用户线程一起工作,因此从整体上看,CMS收集器线程的内存回收过程是与用户线程一起并发执行的。 

 

 

CMS缺点

  • CMS以牺牲CPU资源的代价来减少用户线程的停顿。当CPU个数少于4的时候,有可能对吞吐量影响非常大;

  • CMS收集器无法处理浮动垃圾(Floating Garbage),即第一次标记,认为某个对象不是垃圾,但是在CMS线程和用户线程在并发执行的过程中此对象可能变成了垃圾,那么CMS无法在这次的垃圾回收中将它回收掉。无法处理这些垃圾可能出现”concurrent mode failure“失败而导致另一次Full GC的产生。如果在应用中老年代的增长速度不是太快,可以适当调高-XX:CMSInitiatingOccupancyFractio 的值来提高触发的百分比(不是很懂),以便降低内存回收的次数从而获取更好的性能。要是CMS运行期预留的内存无法满足程序的需要时,虚拟机将启动后备预案,临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样一来停顿的时间就更久了。所以说参数-XX:CMSInitiatingOccupancyFraction 设置太高容易导致大量的”concurrent mode failure“失败,性能反而降低

  • 由于基于MS算法即Mark-Sweep,收集结束时会带来碎片问题,空间碎片过多会给大对象分配带来很大麻烦,望往往出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前进行一次Full GC。CMS收集器提供了一个参数:-XX:+UseCMSCompactAtFullConnection 开关参数,默认是开启的,用于在CMS收集器顶不住要进行 Full GC时同时开启内存碎片的合并整理过程,内存整理的过程是无法并发的并且需要stw,空间碎片问题没有了,但停顿时间不得不变长。

 

收集步骤:方法二

        CMS收集器收集步骤,以下是将上面的四个步骤进一步细分为7个步骤,但是其中有stw的还是只有两个步骤,减少了stw的时间。

  • Phase 1: Initial Mark,这个是CMS两次stop-the-world事件的其中一次,这个阶段的目标是:标记那些直接被GCroot引用或者被年轻代存活对象所引用的所有对象(CMS是针对老年代的)

    • 1583929967459

  • Phase 2:Concurrent Mark,在这个阶段 Garbage Collector会遍历老年代,然后标记所有存活的对象,它会根据上个阶段找到的 GC Roots遍历査找。并发标记阶段,它会与用户的应用程序并发运行并不是老年代所有的存活对象都会被标记,因为在标记期间用户的程序可能会改变一些引用。在下的图中,与阶段1的图进行对比,就会发现有一个对象的引用已经发生了变化

    • 1582896958721

  • Phase 3: Concurrent Preclean,这也是一个并发阶段,与应用的线程并发运行,并不会stop用户线程。在并发运行的过程中,一些对象的引用可能会发生变化,但是这种情况发生时,JVM会将包含这个对象的区域(Card)标记为Diy,这个动作称为Card Marking,在pre-clean阶段,那些能够从Dirty对象到达的对象也会被标记,这个标记做完之后, dirty card标记就会被清除了

    • 1582940994952

  • Phase 4: Concurrent Abortable Preclean,这也是一个并发阶段,但是同样不会影响用户的应用线程,这个阶段是为了尽量承担STW(stop-the-world)中最终标记阶段的工作。这个阶段持续时间依赖于很多的因素由于这个阶段是在重复做很多相同的工作(比如:重复迭代的次数、完成的工作量或者时钟时间等)

  • Phase 5: Final Remark,这是第二个STW阶段,也是CMS中的最后一个,这个阶段的目标是标记老年代所有的存活对象,由于之前的阶段是并发执行的,GC线程可能跟不上应用程序的变化为了完成标记老年代所有存活对象的目标,STW就非常有必要了,这个阶段会比前面的几个阶段更复杂一些

  • Phase 6: Concurrent Sweep,这里不需要STW,它是与用户的应用程序并发运行,这个阶段是:清除那些不再使用的对象,回收它们的占用空间为将来使用

    • 1582941954201

  • Phase 7: Concurrent Reset,这个阶段也是并发执行的,它会重设CMS内部的数据结构,为下次的GC做准备

2.7 总结

        HotSpot虚拟机的组成成分

1582976665504

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值