JVM虚拟机(下) -- JVM垃圾收集算法,GC收集器

前两篇主要讲到JVM的结构及垃圾回收的判断,本篇承接上文,更进一步,说说JVM垃圾收集算法及GC收集器

先说说JVM垃圾收集算法,其中包括复制算法,标记清除算法,标记整理算法及分代回收算法.

复制算法:复制算法将内存分为大小相同的两块,每次使用一块,当需要GC的时候就将还存活的对象复制到另一块内存中,并且将之前的那块内存清空.是不是很眼熟?上一篇中提到的新生代中两块Survivor空间也是这样互相复制.这样做的好处就在于能够消灭内存中的碎片空间,但缺陷也是显而易见的,两块内存中,能够真正被利用的只有一块,也就浪费了一半的空间.详情如下图所示:

JVM虚拟机(下) -- JVM垃圾收集算法,GC收集器

可以看到回收之后的对象碎片空间被消灭了,并且被复制到了另一块内存中.

 标记清除算法:这种算法分为"标记"和"清除"两个阶段,首先标记出需要回收的对象,标记完成后对被标记的对象进行回收.这种算法是最为基础的收集算法.该收集算法会带来效率和空间利用率两个问题.详情如下图所示:

JVM虚拟机(下) -- JVM垃圾收集算法,GC收集器

标记整理算法: 和标记清除收集算法类似,不同在于会将剩余存活下来的对象移动到同一端.详情如下图所示:

JVM虚拟机(下) -- JVM垃圾收集算法,GC收集器

分代回收算法: 当前的虚拟机垃圾收集器使用的都是分代回收算法,该算法本身并没有引出新的思想,只是将内存对象分为几块,一般是JVM堆中分为新生代和老年代,对应不同的区域使用不同的垃圾收集算法.比如目前大部分收集器都是在新生代中使用复制算法,在老年代中使用标记整理算法.只不过新生代的复制算法时并不是按照1:1分配空间而是Eden:From Survivor:To Survivor为8:1:1的比例,这个上一篇有介绍,这里就不再赘述.


再来说GC收集器.GC收集器包括Serial,ParNew,Parallel Scavenge,Serial old,Parallel old,CMS,G1等,接下来对他们一一介绍.

在说收集器之前,先介绍一下工作线程和收集器线程.工作线程可以理解为处理业务的线程,收集器线程则可以认为是JVM内部用于处理回收的线程.

安全点:工作线程需要执行到安全点才能开始进行GC操作,在之前文章讲synchronized 锁膨胀及相关知识点内容时有提到过.

1.Serial,配置命令为-XX:+UseSerialGC.Serial收集器也叫做串行收集器.顾名思义,工作是串行的.如下图所示:

JVM虚拟机(下) -- JVM垃圾收集算法,GC收集器

 Serial收集器新生代使用复制算法,老年代使用标记整理算法,需要注意的是,在该垃圾收集器工作的时候必须暂停其他所有工作线程.也就是出现STW(Stop The Word),直到垃圾回收工作完成才会恢复之前中断的工作线程.这也就意味着每次GC都会有停顿,GC越频繁,停顿越频繁.新生代使用复制算法

2.ParNew,配置命令为-XX:+UseParNewGc.ParNew收集器其实是Serial收集器的多线程版.怎么理解?如下图所示

JVM虚拟机(下) -- JVM垃圾收集算法,GC收集器

相比Serial收集器,在发送GC时,收集器线程由单个变为多个.除了Serial之外,只有它能和CMS收集器配合工作.(CMS后文会介绍).新生代使用复制算法

3.Parallel Scavenge,配置命令为-XX:+UseParallelGC,Parallel Scavenge使用的是复制算法,所以作用于新生代.该收集器的关注点在吞吐量,也就是高效利用CPU,而其他收集器比如CMS(下文会讲),关注的是用户线程的停顿时间.所以Parallel Scavenge给用户提供了很多参数让用户配置到最适合的停顿时间或者吞吐量.

4.Serial old,就是Serial的老年代版本,运行图同Serial,只不过使用的是标记整理算法.

5.Parallel old,是Parallel Scavenge的;老年代版本,同样使用的也是标记整理算法.

6.CMS,配置命令-XX:+UseConcMarkSweepGC,CMS收集器(Concurrent Mark Sweep)是一种追求最短停顿时间的收集器.适合在注重用户体验的应用上使用.而它用于老年代的垃圾回收,是真正意义上的并发收集器,之前介绍的都是并行(这里说的并发与并行指的是用户线程与垃圾收集线程的并发和并行.CMS收集器可以让用户线程与垃圾收集线程同时进行,虽然从CPU时间片上来看并不是同时进行的,但宏观来看是同时.而其他收集器,如ParNew在执行垃圾回收时多个垃圾回收线程在同时执行,但用户线程此时已经处于等待状态了).它的使用算法是"标记清除"算法,但是比其他老年代收集器的标记回收算法来说稍微复杂一些.接下来展开看看.

第一步,初始标记(CMS-initial-mark),暂停所有其他线程(STW),记录与 GC Roots(上一篇有介绍) 直接相连的对象,这一步速度很快.

第二步,并发标记(CMS-concurrent-mark),此时会同时启动GC线程和用户线程,GC线程会去根据上一步得到的结果去获取闭包结构,但是由于有用户线程的运行(会不停更新对象的引用),所以这个闭包结构不能等同于可达对象.所以这个闭包对象无法保证实时性.

(在并发标记之后重新标记之前还存在两个步骤,预清理(CMS-concurrent-preclean),与用户线程同时进行;可被终止的预清理(CMS-concurrent-abortable-preclean),也是与用户线程同时进行)

第三步,重新标记(CMS-remark),重新标记的作用就是为了防止用户线程新产生的引用对象被当做是可回收的对象而被误删除.因为在第一步第二步只是标识都没有涉及到正在运行的用户线程产生的新引用对象,如果不加以修正,那么很可能出现刚被用户线程引用的对象被删除,引发程序错误.采用多线程来提高效率

第四步,并发清除(CMS-concurrent-sweep).此时GC线程依旧和用户线程同时在运行,这时GC线程将会清除被标识的的对象.

 (在并发清除之后还有一步,并发重置(CMS-concurrent-reset),和用户线程同时执行,等待下一次CMS的触发).

总体来说分为四步,细分的话有七步.

JVM虚拟机(下) -- JVM垃圾收集算法,GC收集器

由此可见,整个过程中只有初始标记和重新标记两个阶段是STW,其他阶段用户线程就没有中断过.所以它的优点便是低停顿.

CMS收集器的缺点由此也能看出:

首先对CPU资源敏感,由于执行的线程多了,那么对于CPU来说资源消耗也变多了,也就意味着程序变慢,吞吐量变低.

其次,无法处理浮动垃圾,也就是在GC过程中,和GC线程并发的用户线程所产生的垃圾,这部分无法在本次GC中被回收,但是可以在下一次GC中回收.

最后,由于使用的是标记删除算法,会导致存在大量的碎片空间.可以使用 -XX:CMSFullGCsBeforeCompaction=n来配置,n表示多少次后压缩碎片空间(整理空间).

接下来,说说G1垃圾回收器.

7.G1(Garbage-First),配置命令为-XX:+UseG1GC,是一款面向服务器的垃圾收集器.主要针对配置多个处理器和大容量内存的机器,有极高概率在满足停顿时间要求的同时还具备高吞吐性.是jdk1.7之后可被使用的收集器.G1收集器和其他收集器有一个重大的不同点.对于内存中的新生代和老年代不再是物理隔阂的方式,用户可用自己配置预期停顿时间

比如,在JVM虚拟机(中)--堆,GC机制中有提到过的模型

JVM虚拟机(下) -- JVM垃圾收集算法,GC收集器

 在G1垃圾回收器里已经不复存在了.虽然依旧保留了年轻代和老年代的的概念,但是分步方式已经大不相同.G1垃圾回收器将java堆划分为多个大小相等的独立区域(Region),JVM最多可以拥有2048个Region,一般每个Region的大小为堆的大小除以2048.可使用-XX:G1HeapRegionSize配置.

JVM虚拟机(下) -- JVM垃圾收集算法,GC收集器

在不同的Region中可以存放Eden,Survivor,Old和Humongous,Humongous比较特殊,它指代的是大对象,如果一个Region无法容纳下有一个大对象,那么会给这个大对象分配若干个连续的Region.而判断是否为大对象的规则为,大于Region容量的50%.

而G1的回收机制前部分和CMS类似

JVM虚拟机(下) -- JVM垃圾收集算法,GC收集器

但是需要注意的是G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值大的对象比如一个Region花100ms能回收10M垃圾,另外一个Region花50ms能回收20M垃圾,那么就会优先回收后面这个Region.

那么回收的时候G1是怎么做的呢?在针对每个Region的时候使用的是类似复制算法,比如Region空间总量是2M,某个可回收Region占用空间1M,另一个可回收空间也占用1M,那么回收的时候会将这两个Region复制出来,放在另一个新的Region中,正好能放下.

所以G1拥有可预测停顿空间整合两个特点,可预测停顿便是用户可用自行配置停顿时间,空间整合则是因为虽然整体上看起来G1使用的是标记整理算法,但是在针对局部的Region时,使用的是类似复制算法.可用减少空间碎片.

本篇完.