JVM理论:(二/3)垃圾收集算法垃圾收集器

Posted zjxiang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM理论:(二/3)垃圾收集算法垃圾收集器相关的知识,希望对你有一定的参考价值。

  掌握三种垃圾算法,七种垃圾收集器,了解每种垃圾收集器使用的是哪种垃圾收集算法,以及关于SafePoint的知识点。 

 

垃圾收集算法

1、标记-清除算法(Mark-Sweep)

  先标记(如可达性算法)出所有需要回收的对象,标记完后再统一回收所有被标记的对象。

  缺点:标记和清除过程的效率都不高,且清除后会产生大量不连续的内存碎片。

2、复制算法(Copying)

  将可用内存划分为大小相等的两块,每次只使用其中的一块。当其中一块的内存用完了,就将还存活的对象复制到另一块内存上,然后再把已使用过的内存空间一次清理掉。

  新生代回收的原理就是使用复制算法,HotSpot虚拟机将新生代分为1个Eden和2个Survivor,默认比例是Eden:Survior1:Survior2 = 8:1:1。回收时,将Eden和Survior1中还存活的对象一次性复制到Survior2,然后清理掉Eden和Survior1的空间。由于没办法保证每次回收后存活的对象小于10%,因此要对新生代进行分配担保,即如果另一块Survivor没有空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。

  优缺点:复制算法虽然不用考虑内存碎片的问题,简单高效,但也需要浪费一部分的空间,还有需要考虑分配担保,适用于对象存活率不高的新生代。

3、标记-整理算法(Mark-Compact)

  对于对象存活率较高的老年代,不适合使用复制算法,因为不仅会进行较多的复制,且还需要额外的空间进行分配担保。

  老年代中一般使用“标记—整理”算法,先标记出需要回收的对象,接着让所有存活对象都向一端移动,然后直接清理掉端边界以外的所有内存。

4、分代收集算法(Generational Collection)

  根据Java堆不同年代的区域采用适当的收集算法,新生代中98%的对象朝生夕死,适合用复制算法,只需要复制少量存活对象。老年代中对象存活率高,且没有额外空间对它进行担保,适合用“标记—清除”或“标记—整理”算法。

 

垃圾收集器

  以下7种作用于不同分代的收集器,两收集器间存在连线,表示可以搭配使用。

  技术分享图片

1、Serial收集器

  新生代收集器,单线程,GC时会暂停其他所有的工作线程(stop the world),使用复制算法。

  虚拟机在Client模式下默认的新生代收集器,在单CPU环境下,Serial收集器由于没有线程交互的开销,简单高效。

  开启Serial收集器:-XX:+UseSerialGC,与Serial Old搭配使用的运行过程如下图。

  技术分享图片

2、ParNew收集器  

  新生代收集器,多线程并行,GC时会暂停其他所有的工作线程(stop the world),使用复制算法,是Serial收集器的多线程版。

  许多在Server模式下的虚拟机首选的新生代收集器,因为只有Serial和ParNew收集器能与CMS(老年代)配合工作。选择CMS(-XX:+UseConcMarkSweepGC)后默认的新生代收集器是ParNew,也可用-XX:+UseParNewGC命令指定为ParNew收集器。

  如果是单CPU的环境,Serial会有更好的效果,但随着CPU数量的增加,ParNew更适合。ParNew默认开启的GC线程数与CPU数量相同,通过-XX:ParallelGCThreads可以限制GC线程数。

   

  并行:多条GC线程并行工作,但此时用户线程仍处于等待状态,           即GC线程和用户线程不能同时工作,其中一个线程工作,就需要停止别的线程,一个CPU只执行一条线程。

  并发:用户线程与GC线程同时执行(不一定是并行,可能会交替执行),用户程序继续运行,垃圾收集程序运行于另一个CPU。          以一个CPU的角度,广义上是同时运行的(交替执行)

   与Serial Old搭配使用的运行过程如下图。

   技术分享图片

3、Serial Old收集器

  Serial的老年代版本,单线程,使用“标记-整理”。

  一般给Client模式下的虚拟机使用,如果在Server模式下,一种用途是JDK 1.6以前与Parallel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案

4、Parallel Scavenge收集器

  新生代收集器,多线程并行、使用复制算法。

  看上去与ParNew收集器相似,但不同处是Parallel Scavenge的关注点是能控制吞吐量。CMS的关注点则是尽可能缩短GC时用户线程的停顿时间,低停顿时间适合需要与用户频繁交互的程序,高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。吞吐量是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间),例如虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

  XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间,大于0的毫秒,收集器会尽可能保证GC时间不超过设置值,但是不是设置小了GC就快了,设置小了,GC就频繁了,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的,新生代调小一些,收集300MB新生代肯定比收集500MB快,但这也直接导致垃圾收集发生得更频繁一些,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。

  -XX:GCTimeRatio:直接设置吞吐量大小,大于0且小于100的整数,吞吐量的倒数,若设置为19,那允许的最大GC时间就占总时间的5%(即1/(1+19)),默认值为99,就是允许最大1%(即1/(1+99))的垃圾收集时间。

  -XX:+UseAdaptiveSizePolicy:GC自适应的调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数以提供最合适的停顿时间或者最大的吞吐量。只要把基本的内存数据设置好(如-Xmx设置最大堆),然后使用MaxGCPauseMillis(更关注最大停顿时间)或GCTimeRatio(更关注吞吐量)给虚拟机设立一个优化目标即可,接着具体细节参数就由虚拟机自适应调节。

  与Parallel Old搭配使用的运行过程如下图。   

  技术分享图片

5、Parallel Old收集器

  Parallel Scavenge的老年代版,多线程,“标记-整理”算法。

  JDK1.6前,Parallel Scavenge只能与老年代收集器Serial Old(PS MarkSweep)组合,由于Serial Old无法充分利用服务器多CPU的处理能力,会拖累整体性能。

  JDK1.6后,Parallel Scavenge可与Parallel Old组合,达到名副其实的“吞吐量优先”,在注重吞吐量以及CPU资源敏感的场合可以优先考虑这个组合。

6、CMS收集器(Concurrent Mark Sweep)

  基于“标记—清除”算法,低停顿,并发收集。以获取最短回收停顿时间、低延迟为目标,适用于重视服务响应速度的应用。

主要过程为以下4步——

 (1)初始标记:会经历Stop The World,标记GC Roots能直接关联到的对象,速度很快。

 (2)并发标记:GC Roots Tracing,从GC Roots进行可达性分析,搜索所关联的引用链,可与用户线程并发进行。

 (3)重新标记:也需要Stop The World,因为并发标记阶段程序仍运行,可能会让部分标记产生变动,这个阶段是为了修正并发标记期间因用户程序继续运作而产生变动的那一部分标记,停顿时间比初始标记稍长,但远比并发标记短。

 (4)并发清除:GC线程和用户线程可并发进行。

 CMS运行过程如下图。

  技术分享图片

 因为耗时最长的并发标记和并发清除过程GC线程都可以与用户线程一起工作,所以整体来说,CMS是与用户线程并发执行的。

 

CMS的3个缺点——

 (1)对CPU资源非常敏感,CMS默认启动回收线程数是(CPU数+3)/4,如果CPU数量较多时还好,但如果CPU数量不足4个,GC时将占用近一半的CPU资源,如果本来CPU负载就比较大,还分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了很多,GC时占用过多CPU资源会让总吞吐量降低。

 (2)无法处理浮动垃圾,由于在并发清理阶段,用户线程还会运行,因此就会产生的新垃圾,而这些新垃圾都是在标记后才产生的,这些垃圾就称作浮动垃圾,CMS只能等到下次GC才能处理它们。

  由于垃圾收集阶段用户线程还需要运行,所以就要预留足够的内存给用户线程使用,CMS不能像其他收集器那样等到老年代几乎完全被填满再进行收集,JDK 1.6中,老年代使用了92%的空间后就会激活CMS收集器,通过-XX:CMSInitiatingOccupancyFraction可设置触发比例,若CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”,这时虚拟机会临时启用Serial Old收集器来重新进行老年代的垃圾收集,停顿时间很长。所以-XX:CMSInitiatingOccupancyFraction设置太高容易导致大量“Concurrent Mode Failure”,太低频繁GC也会影响性能。

 (3)会产生大量的空间碎片,因为CMS收集器基于“标记—清除”算法,所以收集结束时会有大量空间碎片产生,对于碎片整理有以下两个参数可以进行调整。

 -XX:+UseCMSCompactAtFullCollection:默认开启,用于在CMS收集器顶不住要进行Full GC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。

 -XX:CMSFullGCsBeforeCompaction:用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理)。

7、G1收集器

  G1收集器和其他收集器的Java堆布局有很大差别,其他收集器的收集范围都是整个新生代或老年代,而G1将整个Java堆划分为多个大小相等的独立区域Region,新生代和老年代不再是物理隔离的。空间分布如下图所示。

  G1仍然采用分代算法,新生代中依然将存活对象拷贝到老年代或者Survivor空间,老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作,这样就不会有CMS内存碎片问题的存在了。图中H的区域代表Humongous,这表示这些Region存储的是巨大对象(humongous object,H-obj),即大小大于等于region一半的对象,H-obj会直接被分配到old gen,防止了反复拷贝移动,如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储,为了能找到连续的H区,有时候不得不启动Full GC。

  技术分享图片

  G1除了追求低停顿外还有另一个特点,能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,有这个特点是因为G1会跟踪各个Region里垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(G1名称的由来)。

    接下来抛出一个问题,也是其他收集器会面临的问题:整个Java堆任意对象间都可能存在引用关系,那么在做可达性判定确定对象是否存活时,是否要扫描整个Java堆?

  G1及其他收集器都是通过Remembered Set来避免全堆扫描的:G1中每个Region都有一个与之对应的Remembered Set,当虚拟机发现程序在对Reference类型的数据进行写操作时,会暂时中断写操作,检查Reference引用的对象是否处于不同Region中(分代中则检查是否老年代中的对象引用了新生代中的对象),如果是,会将引用信息记录到被引用对象所属Region的Remembered Set中,当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

  G1是一款面向服务端应用的垃圾收集器,目标是为了未来替代CMS。

 

  除去Remembered Set的维护,G1运作过程大致分为4个步骤:

  初始标记:标记GC Roots能直接关联到的对象,短暂停顿线程。

  并发标记:从GC Root开始对堆中对象进行可达性分析,找出存活对象,耗时长,可与用户线程并发。

  最终标记:修正在并发标记期间产生变动的那一部分标记记录,虚拟机会将这段时间对象的变化记录在线程的Remembered Set Logs中,最后把Remembered Set Logs的数据合并到Remembered Set中,需要停顿线程,可并行。

  筛选回收:先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,可与用户线程并发,这个阶段只回收一部分Region,时间是用户可控的。

  G1收集器运行图如下。

  技术分享图片

 综合以上,G1有如下特点:

 * 并行与并发:G1能充分利用多个CPU来缩短Stop-The-World停顿的时间,G1收集器仍然可以通过并发的方式让Java程序在GC时继续执行。

 * 分代收集:与其他收集器一样,分代概念在G1中依然得以保留,G1可以不需要其他收集器配合就能独立管理整个GC堆。

 * 空间整合:与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。

 * 可预测的停顿:降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型。

 

 G1与Parallel Scavenge/PS Old相比,最大的好处是停顿时间更加可控,可预测。如果追求低停顿,可以尝试G1,如果追求吞吐量,G1不会有特别的好处。

   G1参数配置:

   -XX:+UseG1GC:指定使用G1收集器;

   -XX:InitiatingHeapOccupancyPercent:当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45;

   -XX:MaxGCPauseMillis:为G1设置暂停时间目标,默认值为200毫秒;

   -XX:G1HeapRegionSize:设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region;

 

参考链接:

  https://blog.csdn.net/tjiyu/article/details/53983650

  https://blog.csdn.net/canot/article/details/51050824

  http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html#important_defaults

    https://www.cnblogs.com/ASPNET2008/p/6496481.html

  https://www.cnblogs.com/oldtrafford/p/6883796.html

    https://blog.csdn.net/baiye_xing/article/details/73743395

    https://www.cnblogs.com/woshimrf/p/jvm-garbage.html

 

 枚举根节点、安全点、安全区

  在总结完垃圾收集算法及垃圾收集器后,还有几个小细节。

1、枚举根节点、安全点

  先回顾一下,能作为GC Roots的节点主要有常量、静态类属性、栈帧中的本地变量表。

  在做可达性分析时,对象能被回收的条件是没有引用来引用它,要判断这点就需要先得到所有的GC Roots节点,如果要在可能达到数百兆的方法区中逐个检查引用,必然会消耗很多时间,HotSpot中用了一种很直接的办法,通过一种叫OopMap的数据结构直接得知哪些地方存着对象引用。

  但不是在所有地方都记录OopMap,只有在安全点处才会记录OopMap信息,每个方法可能会根据safepoint被分成好几段代码,每一段代码一个oopMap,作用域自然也仅限于这一段代码,所以每个方法可能会有好几个oopMap。安全点的选定一般是在那些需长时间执行的地方,如方法调用、循环跳转、异常跳转等。

  值得一提的是做可达性分析时,为了保证一致性必须在到达安全点时才能停顿所有Java执行线程开始GC,即Stop The World,那么抛出一个问题:如何在GC发生时让所有线程(不包括JNI调用的线程)都跑到最近的安全点上再停顿下来?

  有抢先式中断和主动式中断两种方案,抢先式中断是在GC发生时,先把所有线程中断,发现有线程中断的地方不在安全点上,就恢复线程,让它跑到安全点上,现在的虚拟机一般不是用这种方案;主动式中断是在安全点和创建对象需要分配内存的地方设置一个轮询标志,当GC中断线程时,不直接对线程操作,让各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。

2、OopMap与Remembered Set回顾

  结合在G1垃圾收集器里提到的Remembered Set,简单区分一下,OopMap记录的是根节点的引用信息,让枚举根节点时快速准确。

  而根节点是也有可能在老年代中的,就存在这种情况:位于老年代的某个 GC Root,它引用了新生代的某个对象,这个新生代的对象就不能清除,但如果我们只想回收新生代的对象是不是还要去查找老年代的引用呢?为了让我们回收新生代时不用扫描整个堆,就通过Remembered Set来记录位于不同年代对象之间的引用关系

 3、安全区域

  使用安全点Safepoint机制会遇到一个问题:处于Sleep或Blocked状态的线程,无法响应JVM的中断请求,走到安全的地方去中断挂起,JVM也不太可能等待线程重新被分配CPU时间,对于这种情况就需要安全区域Safe Region来解决。

  安全区域是指在一段代码片段中,引用关系不会发生变化,在这个区域中的任意地方开始GC都是安全的,

  在线程执行到安全区域时,首先标识自己已经进入了安全区域,那样在这段时间里JVM要发起GC,就不用管标识自己在安全区域的那些线程了,在线程要离开安全区域时,会检查系统是否完成GC,如果完成线程就可以继续执行,否则要等待收到可以离开安全区域的信号。

 

参考链接:

  https://blog.csdn.net/ifleetingtime/article/details/78934379

  https://www.jianshu.com/p/d0ab167b460d

    https://www.cnblogs.com/strinkbug/p/6376525.html?utm_source=itdadao&utm_medium=referral

    http://dsxwjhf.iteye.com/blog/2201685


以上是关于JVM理论:(二/3)垃圾收集算法垃圾收集器的主要内容,如果未能解决你的问题,请参考以下文章

浅谈JVM垃圾回收器相关知识点

浅谈JVM垃圾回收器相关知识点

JVM 垃圾收集算法

JVM的垃圾收集算法

JVM垃圾收集算法

JVM垃圾收集器的种类