上一篇文章介绍了垃圾回收机制中是如何通过对象引用判断对象是否死亡。本文将继续介绍,接下来垃圾收集器会如何回收这些已经死亡的对象。垃圾收集器的实现有多种多样,针对不同的应用场景可以使用对应的垃圾收集器来进行垃圾回收。

JVM

垃圾收集算法

标记-清除算法

最简单的垃圾收集思路就是我们将发现的死亡的对象都标记出来,然后直接回收对象占用的空间。这种算法简单直接,但是具有明显的缺点:一个是标记和清除过程效率不高;另外会产生大量不连续的内存碎片,当碎片过多,需要分配较大对象的内存时无法找到足够的内存,这会触发再次的垃圾回收。

复制算法

针对标记-清除算法出现的问题,复制算法将内存按容量分为大小相等的两块,每次只用其中的一块。当其中一块内存用完,就将存活的对象复制到另一块上,然后把已使用的空间一次清理。这种确实客服了标记-清除算法的出现的缺点,但是明显这浪费了一半的内存空间,显然是我们不能接受的。而且我们会发现,如果对象存活的时间久,复制算法将会在两块内存上重复的复制这些对象,这是完全不必要的。后面提到的分代收集算法对对象的存活时间进行了考虑。

标记-整理算法

标记-整理算法与标记清除算法不同的是,它同样是将死亡的对象标记出来,但是并不直接清楚,而是将存活的对象都向一端移动,然后直接清理掉端边界以外的内存。这种算法不需要向复制算法那样,浪费一半的内存空间,但是需要进行标记和整理过程。

分代收集算法

分代收集算法根据对象的存活周期的不同将内存划分为几块。一般将Java堆分为新生代和老年代,在新生代中每次垃圾收集都有大量的对象死亡,只有少量存活,这时可以采用复制算法,因为只需要复制少量存活的对象到另一块内存上。而老年代中对象存活率高,则可以采用标记-清楚算法或标记-整理算法。

垃圾收集器

垃圾收集器可以看成是前面垃圾收集算法的具体实现。因为Java中采用的分代垃圾收集算法,所以允许在不同的分代上指定垃圾收集器来进行垃圾回收。如在HotSpot虚拟机1.6版本中,新生代的收集器有Serial、ParNew、Parallel Scavenge;老年代有CMS、Serial Old(MSC)、Parallel Old,还有G1收集器。

Serial

Serial收集器是新生代、使用复制算法、单线程的收集器,从名字看就能看出这个是单线程的收集器,而且当它在进行垃圾回收的时候,需要暂停其他所有工作线程,直到它收集结束。可以想象一下,我们写的Java程序在进行垃圾回收的时候,将会停下所有工作,只进行垃圾回收。这个过程称为”Stop The World”,世界静止了。这也是没有办法避免的,我们不可能允许有人在我们打扫房间的时候,又在房间里扔垃圾,这样我们永远打扫不完。

当然”Stop The World”这个过程的时间是毫秒级的,基本察觉不到,不会像我们电脑开机那么慢。单线程也有单线程的好处,如果只有一个CPU的话,这种收集器的效率是很高的。

ParNew

ParNew收集器是Serial收集器的多线程版本,除了多线程外,其他的垃圾回收机制跟Serial收集器基本都是相同的。这种收集器的在多CPU环境下才会更具有优势。

Parallel Scavenge

Parallel Scavenge收集器也是一个新生代、使用复制算法、并行多线程的收集器。与其他收集器关注点不同的是,其他收集器关注的是尽可能地缩短垃圾收集时用户线程暂停的时间,而Parallel Scavenge收集器的目标是达到一个可以控制的吞吐量。吞吐量是CPU用于运行用户代码的时间与CPU总消耗时间的比值。

停顿时间越短越适合需要与用户交互的程序,而高吞吐量可以最高效率的利用CPU时间,尽快完成程序的运算任务,适合后台运算而不需要太多交互的任务。

Serial Old

Serial Old收集器是Serial收集器的老年代版本,是一个单线程、基于标记-整理算法的收集器。

Parallel Old

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法的收集器,同样也是考虑吞吐量优先。

CMS

CMS(Concurrent Mark Sweep)是一种以获取最短回收停顿时间为目标的收集器。在注重服务响应速度的系统上(如:互联网站、B/S服务端等)应用比较多。这是一种基于标记-清除算法实现的收集器,可以分为四个过程:

  • 初始标记:需要”Stop The World”,只是标记一下GC Roots能直接关联到的对象,速度快。
  • 并发标记:进行GC Roots Tracing的过程,时间比较长。
  • 重新标记:需要”Stop The World”,为了修正并发标记期间,因用户程序继续运行导致标记产生变动的那一部分对象的标记记录,比初始标记慢。
  • 并发清除:并发清除需要回收的对象。

CMS收集器的优点体现在并发收集(部分阶段允许用户工作线程与收集线程同时进行)、低停顿。但是有三个比较突出的缺点:对CPU资源非常敏感,无法处理浮动的垃圾,会产生大量空间碎片。

G1

G1收集器基于标记-整理算法实现,不会产生空间碎片,同时可以精确控制停顿,能够实现基本不牺牲吞吐量的前提下完成低停顿的内存回收。G1收集器将整个Java堆(新生代、老年代)划分为大小固定的独立区域,并追踪这些区域里的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先收集垃圾最多的区域。区域划分和有优先级的区域回收,保证了G1收集器在有限时间内可以获得最高的收集效率。

内存分配与回收策略

对象的内存分配,主要是在堆上分配,新对象主要分配在新生代的Eden区上,少数情况也可能直接分配到老年代上。当对象在新生代的Eden区上分配内存时,没有足够的内存进行分配将触发以此Minor GC。Minor GC的时候,Eden区中的存活对象会变复制到Survivor区中,如果Survivor区中的内存空间不够,将通过分配担保机制提前转移到老年代中。当分配大对象(指需要大量连续内存空间的Java对象)时,很容易直接触发Eden区的Minor GC,特别是出现大量存活不长的大对象。大对象容易导致内存还有空间就提前触发垃圾收集来获得足够的空间。虚拟机提供参数来设置当对象大小超过配置的值,直接将大对象分配到老年代。这样可以防止在Eden区和Survivor区发生大量的内存拷贝。当对象在新生代存活一定时间后,将会变移动到老年代,如果老年代的剩余空间不足时,将会触发Full GC。

  • Minor GC:发生在新生代的垃圾收集动作,大多数Java对象具有朝生夕灭的特性,Minor GC非常频繁,而且速度较快。
  • Full GC:发生在老年代的GC,出现了Full GC时,经常会伴随至少一次的Minor GC,速度较慢。

总结

本文主要介绍了Java虚拟机垃圾回收机制中涉及到的垃圾回收算法以及对应的垃圾回收器。通过了解垃圾回收器的工作原理,能够让我们在针对不同的应用场景,游刃有余地选择合适的垃圾回收器来保证应用的高效运行。同时对于虚拟机中内存分配和回收的了解,能在编程时注意代码质量,防止程序出现频繁的垃圾回收,造成停顿时间增加。