JVM 学习笔记(四):垃圾收集器和内存分配

串行回收器

串行回收器是指使用单线程进行回收的回收器。每次回收时,串行回收只有一个工作线程,对于并行能力较弱的计算机来说,串行回收器的专注性和独占性往往有更好的性能表现。串行回收器可以在新生代和老年代使用,根据作用于不同的空间,分为新生代串行回收器和老年代串行回收器。

新生代串行回收器

串行收集器是所有垃圾回收器中最古老的一种,也是 JDK 中最基本的垃圾回收器之一。串行回收器主要有两个特点:

  • 它仅仅使用单线程进行垃圾回收。
  • 它是独占式的垃圾回收。

在串行收集器进行垃圾回收时,Java 应用程序中的线程都需要暂停,等待垃圾回收的完成。它将造成非常糟糕的用户体验,在实时性要求较高的应用场景中,这种现象往往是不能接受的。

虽然如此,串行回收器却是一个成熟且经过长时间生成环境考验的极为高效的收集器。新生代串行处理器使用复制算法,实现相对简单、逻辑处理特别高效、且没有线程切换的开销。

老年代串行回收器

老年代串行收集器使用的是标记压缩算法。和新生代串行收集器一样,它也是一个串行的、独占式的垃圾回收器。由于老年代垃圾回收通常会使用比新生代回收更长的时间,因此,在堆空间较大的应用程序中,一旦老年代串行收集器启动,应用程序很可能会因此停顿较长的时间。

虽然如此,作为老牌的垃圾回收器,老年代串行回收器可以和多种新生代回收器配合使用,同时它也可以作为 CMS 回收器的备用回收器。

并行回收器

并行回收器在串行回收器的基础上做了改进,它使用多个线程同时进行垃圾回收。对于并行能力强的计算机,可以有效缩短垃圾回收所需要的时间。

新生代 ParNew 回收器

ParNew 回收器是一个工作在新生代的垃圾收集器。它只是简单地将串行回收器多线程化,它的回收策略、算法以及参数和新生代串行回收器一样。ParNew 回收器也是独占式的回收器,在收集过程中,应用程序会全部暂停。但由于并行回收器使用多线程进行垃圾回收,因此,在并发能力比较强的 CPU 上,它产生的停顿时间较短,而在但 CPU 或者并发能力较弱的系统中,并行回收器的效果不会比串行回收器好。

新生代 ParallelGC 回收器

新生代 ParallelGC 回收器也是使用复制算法的收集器。从表面上看,它和 ParNew 回收器一样,都是多线程、独占式的收集器。但是,ParallelGC 回收器有一个重要的特点:它非常关注系统的吞吐量,还支持一种自适应的 GC 调节策略。

老年代 ParallelOldGC 回收器

老年代 ParallelOldGC 回收器也是一种多线程并发的收集器。和新生代 ParallelGC 回收器一样,它也是一种关注吞吐量的收集器。ParallelOldGC 回收器使用标记压缩算法。搭配新生代 ParallelGC 回收器使用。

CMS 回收器

CMS 回收器主要关注于系统停顿时间。CMS 是 Concurrent Mark Sweep 的缩写,意为并发标记清除。它使用的是标记清除算法,同时它又是一个使用多线程并行回收的垃圾回收器。

CMS 工作时,主要步骤有:初始标记、并发标记、预清理、重新标记、并发清理和并发重置。其中初始标记和重新标记是独占系统资源的,而预清理、并发清楚和并发重置是可以和用户线程一起执行的。

根据标记清除算法,初始标记、并发标记和重新标记都是为了标出需要回收的对象。并发清理则是在标记完成后,正式回收垃圾对象。并发重置是指回收完成后,重新初始化 CMS 数据结构和数据,为下一次垃圾回收做好准备。并发标记、并发清理和并发重置都是可以和应用程序线程一起执行的。

由于 CMS 回收器不是独占式的回收器,在 CMS 回收过程中,应用程序仍然不停地工作。在应用程序工作过程中,又会不断地产生垃圾。这些新生成的垃圾在当前 CMS 回收过程中是无法清除的。同时,因为应用程序没有中断,所以在 CMS 回收过程中,还应该确保应用程序有足够的内存可用。因此,CMS 回收器不会等待堆内饱和时才进行垃圾回收,而是当堆内存使用率达到某一个阀值时便开始进行回收,以确保应用程序在 CMS 工作过程中,依然有足够的空间支持应用程序运行。

G1 回收器

G1 回收器(Garbage-First)是在 JDK1.7 中正式使用的垃圾回收器,从长期目标来看,它是为了取代 CMS 回收器。G1 回收器拥有独特的垃圾回收策略,这和之前提到的回收器截然不同。从分代上看,G1 依然属于分代回收器,它会区分年轻代和老年代,依然有 eden 区和 survivor 区,但从堆的结构上看,它并不要求整个 eden、年轻代或者老年代都连续。它使用了分区算法。作为 CMS 的长期替代方案,G1 同时使用了全新的分区算法,其特点如下:

  • 并行性:G1 在回收期间,可以由多个 GC 线程同时工作,有效利用多核计算能力。
  • 并发性:G1 拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此一般来说,不会在整个回收期间阻塞应用程序。
  • 分代 GC:G1 依然是一个分代收集器,但是和之前回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,它们或者工作在年轻代,或者工作在老年代。因此,这里是一个很大的不同。
  • 空间整理:G1 在回收过程中,会进行适当的对象移动,不像 CMS,只是简单地标记清理对象,在若干次 GC 后,CMS 必须进行一次碎片整理。而 G1 不同,它每次回收都会有效地复制对象,减少空间碎片。
  • 可预见性:由于分区的原因,G1 可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿也能得到较好的控制。

G1 收集器将堆进行分区,划分为一个个的区域,每次收集的时候,只收集其中几个区域,以此来控制垃圾回收产生的一次停顿时间。

G1 的收集过程可能有 4 个阶段:

  • 新生代 GC
  • 并发标记周期
  • 混合收集
  • 如果需要,可能会进行 Full GC

G1 的新生代 GC

新生代 GC 的主要工作是回收 eden 区和 survivor 区。一旦 eden 区被占满,新生代 GC 就会启动。新生代 GC 收集前后的堆数据如图所示。可以看到,新生代 GC 只处理 eden 和 survivor 区,回收后,所有的 eden 区都应该被清空,而 survivor 区会被收集一部分数据,但是应该至少仍然存在一个 survivor 区,类比其他的新生代收集器,这一点似乎并没有太大变化。另一个重要的变化是老年代的区域增多,因为部分 survivor 区或者 eden 区的对象可能会晋升到老年代。

JVM-G1

G1 的并发标记周期

G1 的并发阶段和 CMS 有点类似,它们都是为了降低一次停顿时间,而将可以和应用程序并发的部分单独提取出来执行。

并发标记周期可以分为以下几步:

  • 初始标记
  • 根区域扫描
  • 并发标记
  • 重新标记
  • 独占清理
  • 并发清理阶段

混合回收

在并发标记周期中,虽然有部分对象被回收,但是总体上说,回收的比例是相当低的。但是在并发周期后,G1 已经明确指定哪些区域含有比较多的垃圾对象,在混合回收阶段,就可以专门针对这些区域进行回收。当然,G1 会优先回收垃圾比较较高的区域,因为回收这些区域的性价比较高。而这些正是 G1 名字的由来。G1 全称为 Garbage First Garbage Collector,直译为垃圾优先的垃圾回收器,这里的垃圾优先指的是回收时优先选取垃圾比例最高的区域。这个阶段叫做混合回收,是因为在这个阶段,既会执行正常的年轻代 GC,又会选取一些被标记的老年代区域进行回收,它同时处理了新生代和老年代。因为新生代 GC 的原因,eden 区域必然被清空,此外,有两块被标记位 G 的垃圾比例最高的区域被清理。被清理的区域中的存活对象会被移动到其他区域,这样的好处是可以减少空间碎片。

必要时的 Full GC

和 CMS 类似,并发收集由于让应用程序和 GC 线程交替工作,因此总是不能完全避免在特别繁忙的场合会出现在回收过程中内存不充足的情况。当遇到这种情况时,G1 也会转入一个 Full GC 进行回收。