Java的垃圾收集算法及GC收集器

529 阅读9分钟

一、垃圾收集算法

1、标记-清除算法

最基础的垃圾收集算法是“标记-清除”(Mark-Sweep)算法,该算法分为“标记”和“清除”两个阶段。

  • 首先标记出所有需要回收的对象
  • 在标记完成后统一回收所有被标记的对象。

不足:

效率问题:标记和清除两个所处的效率都不高;

空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一种垃圾收集动作。




2、复制算法

目标:为了解决效率问题

将可用内存按容量大小划分为大小相同的两块,每一次只使用其中的一块。当一块内存使用完了,就将还存活的对象复制到另一块上面,然后再把已使用的内存空间一次性清除掉。这样使用每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况。

不足:内存缩小为原来的一半。


因为在堆的新生代的对象98%都是“朝生夕死”,因而不需要按照1:1的比列来划分内存空间,而是将内存分为较大的Eden空间和两个较小的Survivor空间(from,to),每次使用Eden和其中一个SUrvivor。HotSpot虚拟机中默认Eden和Survivor的大小比例为8:1。


3、标记-整理算法

复制收集算法在对象存活率较高时,就要进行多次的复制操作,效率就会降低。根据老年代的特点,提出了“标记-整理”算法。


标记过程仍然和“标记-清除”算法一样,但后续操作不是征集对可回收对象进行整理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。



4、分代收集算法

Java堆一般分为新生代和老年代,再根据每个年代的特点采用最适当的收集算法。

新生代中,每次垃圾收集时都有大批对象死去,只有少量存活,那就选择复制算法

老年代中,因为对象存活率很高、没有额外空间对它进行分配担保,就必须采用“标记-清除”算法或“标记-整理”算法进行内存回收。


二、垃圾回收机制的一些知识

1、JVM中的年代

JVM中分为年轻代和老年代

HotSpot JVM把年轻代分为三部分:1个Eden区和2个Survivor区(分别为From和to)。默认是8:1。


一般情况下:新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,若仍然存活,则移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄则增加1岁,当它的年龄增加到一定程度时,就会移动到老年代。

因为年轻代中的对象基本都是朝生夕死(98%),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还存活的对象复制到另外一块上面。

复制算法不会产生内存碎片。


GC开始的时候,对象只存在Eden区“From”的Survivor区Survivor区的“To”空的,紧接着进行GC,Eden区中所有存活对象都会被复制到“To”,而在“From”区中,仍存活的对象根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,通过-XX:MaxTenuringThreshold来设置)的对象会移动到老年代,没有达到阈值则移动到“To”区域。

经过这次GCEden区和From区都被清空。这时,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域为空,Minor GC会一直这样重复,直到“To”区被填满,“To”区被填满后,会将所有对象移动到老年代。


2、Minor GC和Full GC区别

Minor GC指发生在新生代的垃圾收集动作,该动作非常频繁

Full GC/Major GC指发生在老年代的垃圾收集动作出现Major GC,常会伴随着至少一次的Minor GC。Major GC的速度一般会比Minor GC慢10倍以上。


3、空间分配担保

在发生Minor GC之前,虚拟机会先检测老年代最大可用的连续空间是否大于新生代所有对象的总空间,若该条件成立,则Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。若允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年嗲对象的平均大小,如果大于,则尝试进行一次Minor GC,尽管本次Minor GC有风险。若小于,或HandlePromotionFaiure设置不允许冒险,那这时改为进行一次Full GC


三、垃圾收集器


上图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,则说明他们可以搭配使用。


1、Serial收集器

最基本、发展历史最悠久的收集器。这是一个单线程收集器

但它的“单线程”的意思并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。


是虚拟机运行在Client模式下的默认新生代收集器。

优点简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专门做垃圾收集自然可以获得最高的单线程效率。


2、ParNew收集器

ParNew收集器其实是Serial收集器的多线程版本。

是许多运行在server模式下的虚拟机中首选的新生代收集器,其中一个与性能无关但很重要的原因是,除了serial收集器外,目前只有它能与CMS收集器配合工作。

ParNew收集器默认开启的收集线程数与CPU的数量相同,如下图:



3、Parallel Scavenge收集器

Parallel Scavenge收集器是一个新生代收集器,使用复制算法,优势并行的多线程收集器

最大特点是:Parallel Scavenge收集器的目标是达到一个可控制的吞吐量

所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗的时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合后台运行而不需要太多交互的任务


4、Serial Old收集器

Serial Old是Serial收集器的老年代版本,同样是一个单线程收集器,使用“标记-整理”算法。该收集器的主要意义也是在于给Client模式下虚拟机使用。

如果在server模式下,它主要的用途在于

  • Parallel Scavenge收集器搭配使用
  • 作为CMS收集器后备预案,在并发收集发生Concurrent Mode Failure使用


5、Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法

注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge + Parallel Old收集器


6、CMS(Concurrent Mark Sweep)收集器

是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集器与用户线程同时工作

关注点:尽可能地缩短垃圾收集时用户线程的停顿时间

CMS收集器时基于“标记-清除”算法实现的,整个过程分为4步骤:

  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清除


初始标记和重新标记这两个步骤仍然需要“Stop the World”。

初始标记仅仅只标记一下GC Root能直接关联到的对象,速度很快。

并发标记则是进行GC Roots 跟踪的过程。

重新标记则是为了修正并发标记期间因用户程序运行而导致标记产生变动的那一部分对象的标记几率,这一阶段的停顿时间一般比初始标记稍长,但远比并发标记时间短。

耗时最长的是并发标记、并发清除,但这两个阶段与用户线程一起工作。


缺点:

  • CMS收集器对CPU资源非常敏感
  • CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure"而导致名一个Full GC尝试。浮动垃圾产生并发清除阶段。当出现上述失败时,虚拟机启动后备预案Serial Old。
  • CMS是一个基于“标记-清除”算法实现的收集器,会产生大量空间碎片问题。


7、G1收集器

当前收集器技术发展的最前沿成果之一。是一款面向服务端应用的垃圾收集器。

特点:

  • 并行与并发

能充分利用多CPU,多核环境的硬件优势,缩短Stop The World停顿时间,同时可以通过并发的方式让Java程序继续执行

  • 分代收集

可以不需要其他收集器的配合管理整个堆,但是仍采用不同的方式来处理分代的对象

  • 空间整合

G1从整体上来看,采用基于“标记-整理”算法实现收集器

G1从局部上来看,采用基于“复制”算法实现。

  • 可预测停顿

使用G1收集器时,Java堆内存布局与其他收集器有很大差别,它将整个Java堆划分成为多个大小相同的独立区域。G1跟踪各个区域里面的垃圾堆积的价值大小(回收所获得的空间大小以回收所需要的时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的区域


上述所有图片来源于:github.com/LRH1993/and…

侵权请告知,立刻删除