JAVA学习-JVM垃圾回收算法&垃圾收集器

常用的GC算法

1.引用计数法

给一个对象添加一个引用计数器,每当有一个地方引用它时,计数器的值就加1,当引用失效时计数器就减1,当计数器的值为0时,代表对象不再被使用,可以进行回收。

特点

引用计数法实现简单,判断效率也很高,但是它很难解决对象之间互相引用的问题。如两个对象objA和objB都有一个字段instance,

objA.instance=objB;
objB.instance=objA;

除了这里互相引用其余地方都没有任何引用,这两个对象都是不可访问的对象,但他们的计数都不是0.引用计数法无法通知GC收集器收集。

2.可达性算法

这个算法的思想是通过一系列叫做“GC Root”的节点,从这些节点开始向下搜索,搜索到的路径叫做引用链,当一个对象到GC Root没有任何引用链相连(图论的角度,没有从GC Root到该对象的路径),表明该对象不可达。可以被回收

GC Root对象

虚拟机栈(栈帧中的本地变量表)中引用的的变量
方法区类静态属性的变量
方法区常量引用的对象
本地方法栈中(即一般说的Native方法)引用的变量

标记清除法

标记清除法有两个阶段:标记和清除
标记阶段:标记阶段会为所有活动对象打上标记,首先标记通过根直接引用的对象,然后递归的别偶记通过指针数组可以访问的对象,有些函数可能会多次调用,为避免重复标记,每次标记前会检查是否已标记,若已标记,则不处理。
清除阶段:清除标记阶段标记的对象

标记清除算法图
特点

1.标记跟清除两个阶段效率都不高
2.存在内存碎片问题

复制算法

为了解决标记清除的效率问题,产生了复制算法,首先将内存分成大小相等的两块,每次使用其中的一块,当内存不够发生GC时,将存活的对象复制到未使用的一块中,一次性清除已使用过的那一块的所有。

复制算法图
特点

1.每次都对整个半区内存进行回收,内存分配也不需要考虑内存碎片等复杂情况
2.实现简单,运行高效
3.代价是将内存减少到了原来的一半
4.现在商业的虚拟机都采用复制算法对分代思想的新生代进行收集。
5.当存活率较高时,要复制的对象就很多,复制算法的效率将会变得很低。

标记整理算法

为了解决对象存活率比较高的情况下,复制算法效率变得很低的问题。还有如果不想浪费50%的空间的话,为了应对一些极端情况(如100%对象存活)就需要额外的内存进行担保,(下面分代算法中的新生代采用复制算法,就需要老年代做担保)。基于上面这些复杂算法的特点,所以老年代一般不采用复制算法进行垃圾回收,而是采用标记整理算法。

标记整理算法与标记清除算法的区别是后续步骤不是对垃圾进行直接的清除,而是将存活对象移向内存的一端,然后直接清除掉边界外的垃圾内容,如下图所示

标记整理算法图
特点

1.在标记-清除的基础上还需进行对象的移动,成本相对较高
2.不会产生内存碎片。
3.一般在老年代的回收中会采用。(如CMS垃圾回收器可以设置在进行多少次标记清除算法之后可以进行一次标记整理算法)

分代算法

这种方法根据对象的存活周期不同,将内存划分为几块,一般将内存分为新生代、老年代。这样可以根据不同年代的特点使用不同的回收算法,新生代一般都是朝生夕灭的对象,只有少量对象存活所以可以选择复杂算法。而老年代对象的存活率较高,又没有空间担保,所以一般采用标记清除或标记整理算法。

收集器的种类

Serial收集器&Serial old收集器

Serial收集器是一款单线程收集器主要是用来对新生代进行垃圾回收,采用的是复制算法,与Serial old配合使用时Serial old对老年代进行垃圾回收,采用标记整理算法。

Serial收集器简单高效但停顿现象严重(Stop the world现象),对于运行在Client模式下的虚拟机是一种很好的选择

Serial/Serial old收集器

ParNew收集器

ParNew收集器是Serial收集器的多线程版本,也是新生代的一款收集器,采用复制算法,与Serial收集器相比除了采用多线程收集外,没有多大区别。还有一点需要注意,除了Serial收集器,目前只有ParNew收集器能与CMS配合工作
 ParNew收集器在单CPU环境中绝对没有Serial的效果好,由于存在线程交互的开销,该收集器在超线程技术实现的双CPU中都不能一定超过Serial收集器。默认开启的垃圾收集器线程数就是CPU数量,可通过-XX:parallelGCThreads参数来限制收集器线程数

ParNew/Serial old

Parallel Scavenge(并行回收GC)收集器

Parallel Scavenge(并行回收GC)收集器是一款新生代的、采用复制算法的并行收集器,parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间)

Parallel Scavenge/Serial Old

短停顿时间适合和用户交互的程序,体验好。高吞吐量适合高效利用CPU,主要用于后台运算不需要太多交互。
  提供了两个参数来精确控制吞吐量:1.最大垃圾收集器停顿时间(-XX:MaxGCPauseMillis 大于0的毫秒数,停顿时间小了就要牺牲相应的吞吐量和新生代空间),2.设置吞吐量大小(-XX:GCTimeRatio 大于0小于100的整数,默认99,也就是允许最大1%的垃圾回收时间)。
  还有一个参数表示自适应调节策略(GC Ergonomics)(-XX:UseAdaptiveSizePolicy)。就不用手动设置新生代大小(-Xmn)、Eden和Survivor区的比例(-XX:SurvivorRatio)新生老年代对象大小(-XX:PretenureSizeThreshold),会根据当前系统的运行情况手机监控信息,动态调整停顿时间和吞吐量大小。也是其与PreNew收集器的一个重要区别,也是其无法与CMS收集器搭配使用的原因(CMS收集器尽可能地缩短垃圾收集时用户线程的停顿时间,以提升交互体验)。

Parallel Old(并行GC)收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法,JDK1.6才提供。
  由于之前有一个Parallel Scavenge新生代收集器,,但是却无老年代收集器与之完美结合,只能采用Serial Old老年代收集器,但是由于Serial Old收集器在服务端应用性能上低下,其吞吐量反而不一定有PreNew+CMS组合。

Parallel Scavenge/Parallel old

CMS(并发GC)收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是HotSpot虚拟机中的一款真正意义上的并发收集器,第一次实现了让垃圾回收线程和用户线程(基本上)同时工作。用CMS收集老年代的时候,新生代只能选择Serial或者ParNew收集器。
  CMS收集器是基于“标记-清除”算法实现的,整个收集过程大致分为4个步骤:
①.初始标记(CMS initial mark)
②.并发标记(CMS concurrenr mark)
③.重新标记(CMS remark)
④.并发清除(CMS concurrent sweep)
其中初始标记、重新标记这两个步骤仍然需要停顿其他用户线程(Stop The World)。初始标记仅仅标记出GC ROOTS能直接关联到的对象,速度很快,并发标记阶段是进行GC ROOTS 根搜索算法阶段,会判定对象是否存活。而重新标记阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会被初始标记阶段稍长,但比并发标记阶段要短。
耗时最长的是并发标记与并发清除,是与用户线程一起执行的,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

CMS收集器

CMS收集器的优点:并发收集、低停顿,但是CMS还远远达不到完美
三个缺点
CPU敏感
  1.CMS收集器对CPU资源非常敏感。在并发标记、并发清除阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致应用程序变慢,总吞吐量下降。CMS默认启动的回收线程数是:(CPU数量+3) / 4。收集器线程所占用的CPU数量为:(CPU+3)/4=0.25+3/(4*CPU)。因此这时垃圾收集器始终不会占用少于25%的CPU,因此当进行并发阶段时,虽然用户线程可以跑,但是很缓慢,特别是双核CPU的时候,已经占用了5/8的CPU,吞吐量会很低。为了解决这种情况,产生了“增量式并发收集器”(Incremental Concurrent Mark Sweep/i-CMS)。就是采用抢占方式来模拟多任务机制,就是在并发(并发标记、并发清除)阶段,让GC线程、用户线程交替执行,尽量减少GC线程独占CPU,这样垃圾收集过程更长,但是对用户程序影响小一些。实际上i-CMS效果很一般,目前已经被声明为“deprecated”。

2.CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure“,失败后而导致另一次Full GC的产生。在并发清理阶段用户线程还在执行,会产生新的垃圾,这部分垃圾无法在这次回收中清理,而且用户线程执行,需要给用户线程预留内存以供执行需要,在默认设置下,CMS收集器在老年代使用了68%的空间时就会被激活,也可以通过参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以降低内存回收次数提高性能。

Concurrent Mode Failure解决方式
要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置的过高将会很容易导致“Concurrent Mode Failure”失败,性能反而降低。

内存碎片问题及解决
 3.CMS是基于“标记-清除”算法实现的收集器,为了解决存在很多内存碎片导致内存空间找不到连续的空间来分配不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数,用于在Full GC之后增加一个内存碎片的合并整理过程,但是内存整理过程是无法并发的,因此解决了空间碎片问题,却使停顿时间变长。还可通过-XX:CMSFullGCBeforeCompaction参数设置执行多少次不压缩的Full GC之后,跟着来一次碎片整理过程(默认值是0,每次FullGC都进行整理)。

G1收集器

G1(Garbage First)收集器是JDK1.7提供的一个新的面向服务端应用的垃圾收集器,其目标就是替换掉JDK1.5发布的CMS收集器。
优点
  1.并发与并行:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或CPU核心)来缩短停顿(Stop The World)时间。
  2.分代收集:G1不需要与其他收集器配合就能独立管理整个GC堆,但他能够采用不同方式去处理新建对象和已经存活了一段时间、熬过多次GC的老年代对象以获取更好收集效果。
  3.空间整合:从整体来看是基于“标记-整理”算法实现,从局部(两个Region之间)来看是基于“复制”算法实现的,但是都意味着G1运行期间不会产生内存碎片空间,更健康,遇到大对象时,不会因为没有连续空间而进行下一次GC,甚至一次Full GC。
  4.可预测的停顿:降低停顿是G1和CMS共同关注点,但G1除了追求低停顿,还能建立可预测的停顿模型,可以明确地指定在一个长度为M的时间片内,消耗在垃圾收集的时间不超过N毫秒
  5.跨代特性:之前的收集器进行收集的范围都是整个新生代或老年代,而G1扩展到整个Java堆(包括新生代,老年代)。
那么是怎么实现的呢?
如何实现新生代和老年代全范围收集
其实它的Java堆布局就不同于其余收集器,它将整个Java堆划分为多个大小相等的独立区域(Region),仍然保留新生代和老年代的概念,可是不是物理隔离的,都是一部分Region(不需要连续)的集合。

如何建立可预测的停顿时间模型
是因为有了独立区域Region的存在,就避免在Java堆中进行全区域的垃圾收集,G1跟踪各个Region里面的垃圾堆积的价值大小(回收可以获得的空间大小和回收所需要的时间的经验值),后台维护一个优先队列,根据每次允许的收集时间,优先回收价值最大的Region(Garbage-First理念)。因此使用Region划分内存空间以及有优先级的区域回收方式,保证了有限时间获得尽可能高的收集效率。

如何保证垃圾回收真的在Region区域进行而不会扩散到全局
由于Region并不是孤立的,一个Region的对象可以被整个Java堆的任意其余Region的对象所引用,在做可达性判定确定对象是否存活时,仍然会关联到Java堆的任意对象,G1中这种情况特别明显。而以前在别的分代收集里面,新生代规模要比老年代小许多,新生代收集也频繁得多,也会涉及到扫描新生代时也会扫描老年代的情况,相反亦然。解决:G1收集器Region之间的对象引用以及新生代和老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(分代的例子中就检查是否老年代对象引用了新生代的对象),如果是则通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中,当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可避免全堆扫描。
忽略Remembered Set的维护,G1的运行步骤可简单描述为:
①.初始标记(Initial Marking)
②.并发标记(Concurrenr Marking)
③.最终标记(Final Marking)
④.筛选回收(Live Data Counting And Evacution)
1.初始标记:初始标记仅仅标记GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新的对象。这阶段需要停顿线程,不可并行执行,但是时间很短。
2.并发标记:与CMS一致
3.最终标记:此阶段是为了修正在并发标记期间因为用户线程继续运行而导致标记产生变动的那一份标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这段时间需要停顿线程,但是可并行执行。
4.筛选回收:对各个Region的回收价值和成本进行排序,根据用户期望的GC停顿时间来制定回收计划。
  如果现有的垃圾收集器没有出现任何问题,没有任何理由去选择G1,如果应用追求低停顿,G1可选择,如果追求吞吐量,和Parallel Scavenge/Parallel Old组合相比G1并没有特别的优势。

推荐阅读更多精彩内容