JVM 探秘 6:垃圾收集器

垃圾收集器

垃圾收集算法是是内存回收的方法论,垃圾收集器是内存回收的具体实现。不同的虚拟机会有不同的垃圾收集器的实现,我们主要讨论的是默认的 HotSpot 虚拟机,这个虚拟机包含的垃圾收集器如下图;

image

如上图所示,一共有 7 种垃圾收集器,如果两个垃圾收集器之间有双箭头连线,则两个垃圾收集器可搭配使用。上面是新生代的收集器,下面是老年代的收集器。每个垃圾收集器都有各自适合的使用场景。

Serial 收集器

Serial是一个“单线程”的新生代收集器,使用复制算法,它只会使用一个 CPU 或者一条收集器线程去完成垃圾收集工作,并且它在垃圾收集时,必须暂停所有其他的工作线程,直到它收集结束。“Stop The World”会在用户不可见的情况下,把用户的工作线程全部停掉,这往往是令人难以接受的。

下图是 Serial/Serial Old 收集器运行示意图:

image

上图中,新生代是Serial收集器采用复制算法,老年代是Serial Old收集器采用标记 - 整理算法。Serial 虽然是一个缺点鲜明的收集器,但它依然是虚拟机在 Client 模式下的默认收集器,它也有优点,比如简单高效(与其他收集器单线程相比),对于单个 CPU 来说,Serial 由于没有线程交互的开销,效率比较高,对于桌面应用来说,分配给虚拟机的内存不会很大,收集时的停顿也是在可接受范围内的。

ParNew 收集器

ParNew收集器是 Serial 收集器的多线程版本,也是使用复制算法的新生代收集器,它除了使用多条线程进行垃圾收集以外,其他的比如收集器的控制参数、收集算法、Stop-The-World、对象分配规则、回收策略都和 Serial 收集器完全一样。

下图是 ParNew/Serial Old 收集器运行示意图:

image

上图中,新生代是ParNew收集器采用复制算法,老年代是Serial Old收集器采用标记 - 整理算法。ParNew 是许多 Server 模式下虚拟机的首选新生代收集器,多是因为它能与CMS收集器配合工作。CMS 收集器是 HotSpot 虚拟机中第一个并发的垃圾收集器,CMS 第一次实现了让用户线程与垃圾收集线程同时工作。

简单介绍下垃圾收集中的并行与并发概念:

  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程是等待状态。
  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行,用户程序运行的同时,垃圾收集程序运行于另一个 CPU 上。

Parallel Scavenge 收集器

Parallel Scavenge也是使用复制算法的新生代收集器,并且也是一个并行的多线程收集器。Parallel 收集器跟其它收集器关注 GC 停顿时间不同,它关注的是吞吐量。低停顿时间适合需要与用户交互的程序,而高吞吐量可以高效率的利用 CPU 时间,能尽快完成运算任务,适合用于后台计算较多而交互较少的任务。

  • 吞吐量(Throughput):CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。

Parallel 收集器提供了两个虚拟机参数用以控制吞吐量,-XX:MaxGCPauseMillis参数可以控制垃圾收集的最大停顿时间,-XX:GCTimeRatio参数可以直接设置吞吐量大小。

-XX:MaxGCPauseMillis的值是一个大于 0 的毫秒数,使用它减小 GC 停顿时间是牺牲吞吐量和新生代空间换来的,例如系统把新生代调小,收集 300M 的新生代肯定比 500M 的快,这也导致垃圾收集发生的更频繁,原来 10 秒收集一次每次停顿 100 毫秒,现在 5 秒收集一次每次停顿 70 毫秒,停顿时间下降了,但是吞吐量也下降了。

-XX:GCTimeRatio的值是一个 0 到 100 的整数,通过它我们告诉 JVM 吞吐量要达到的目标值,-XX:GCTimeRatio=N指定目标应用程序线程的执行时间 (与总的程序执行时间) 达到 N/(N+1) 的目标比值。例如,它的默认值是 99,就是说要求应用程序线程在整个执行时间中至少 99/100 是活动的(GC 线程占用其余的 1/100),也就是说,应用程序线程应该运行至少 99% 的总执行时间。

除这两个参数外,还有一个参数-XX:-UseAdaptiveSizePolicy值得关注,这是一个开关参数,当它打开之后,就不需要手工指定新生代大小 (-Xmn)、Eden 与 Survivor 区的比例 (-XX:SurvivorRatio)、晋升老年代对象年龄 (-XX:PretenureSizeThreshold) 等细节参数了,虚拟机会根据系统的运行情况收集性能监控信息,动态的调整这些参数来提高 GC 性能,这种调节方式称为GC 自适应调节策略。这个参数是默认激活的,自适应行为也是 JVM 优势之一。

Serial Old 收集器

Serial Old是 Serial 收集器的老年代版本,同样是一个“单线程”收集器,使用标记 - 整理算法。这个收集器主要是给 Client 模式下的虚拟机使用,Server 模式下还有两个用途,一个是在 JDK1.5 及之前的版本中与 Parallel Scavenge 收集器搭配使用,另一个是作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。工作过程请看 Serial 收集器部分的 Serial/Serial Old 收集器运行示意图。

Parallel Old 收集器

Parallel Old收集器是 Parallel Scavenge 的老年代版本,使用多线程标记 - 整理算法。此收集器在 JDK1.6 中开始出现,在 Parallel Old 出现之前,只有 Serial Old 能够与 Parallel Scavenge 收集器配合使用。由于 Serial Old 这种单线程收集器的性能拖累,导致在老年代比较大的场景下,Parallel Scavenge 和 Serial Old 的组合吞吐量甚至还不如 ParNew 加 CMS 的组合。而有了 Parallel Old 收集器之后,Parallel Scavenge 与 Parallel Old 成了名副其实的吞吐量优先的组合,在注重吞吐量和 CPU 资源敏感的场景下,都可以优先考虑这对组合。

下图是 ParNew/Serial Old 收集器运行示意图:

image

CMS 收集器

CMS(Concurrent Mark Sweep) 收集器是基于标记 - 清除算法的老年代收集器,它以获取最短回收停顿时间为目标。CMS 是一款优秀的收集器,特点是并发收集、低停顿,它的运行过程稍微复杂些,分为 4 个步骤:

  1. 初始标记(CMS initial mark)
  2. 并发标记(CMS concurrent mark)
  3. 重新标记(CMS remark)
  4. 并发清除(CMS concurrent sweep)

4 个步骤中只有初始标记、重新标记这两步需要“Stop The World”。初始标记只是标记一下 GC Roots 能直接关联的对象,速度很快。并发标记是进行 GC Roots Tracing 的过程,也就是从 GC Roots 开始进行可达性分析。重新标记则是为了修正并发标记期间因用户线程继续运行而导致标记发生变动的那一部分记录。并发清理当然就是进行清理被标记对象的工作。

下图是 CMS 收集器运行示意图:

image

整个过程中,并发标记与并发清除过程耗时最长,但它们都可以与用户线程一起工作,所以整体上说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。

但是 CMS 收集器也并不完美,它有以下 3 个缺点:

  1. CMS 收集时对CPU 资源非常敏感,并发阶段虽然不会导致用户线程停顿,但是会因为占用 CPU 资源导致应用程序变慢、总吞吐量变低。
  2. CMS 收集器无法处理浮动垃圾(Floating Garbage),可能会产生 Full GC。浮动垃圾就是在并发清理阶段,依然在运行的用户线程产生的垃圾。这部分垃圾出现在标记过程之后,CMS 无法在当次集中处理它们,只能等下一次 GC 时清理。
  3. CMS 是基于标记 - 清除算法的收集器,可能会产生大量的空间碎片,从而无法分配大对象而导致 Full GC 提前产生。

G1 收集器

G1(Garbage-First) 收集器是面向服务端应用的垃圾收集器,它被寄予厚望以用来替换 CMS 收集器。在 G1 之前的收集器中,收集的范围要么是整个新生代要么就是老年代,而 G1 不再从物理上区分新生代老年代,G1 可以独立管理整个 Java 堆。它将 Java 堆划分为多个大小相等的独立区域(Region),虽然还有新生代老年代的概念,但不再是物理隔离的,而都是一部分 Region(不需要连续)的集合。

与其他收集器相比,G1 收集器的特点有:

  1. 并行与并发:G1 能充分利用多 CPU 或者多核心的 CPU,来缩短 Stop The World 的停顿时间。
  2. 分代收集:虽然 G1 收集器可以独立管理整个 GC 堆,但它能采用不同的方式处理“新对象”和“老对象”,以达到更好的收集效果。
  3. 空间整合:G1 从整体看是基于标记 - 整理算法的,从局部看(两个 Region 之间)是基于复制算法实现的,这两个算法在收集时都不会产生空间碎片,这样就有连续可用的内存用以分配大对象。
  4. 可预测的停顿:G1 除了追求低停顿外,还能建立可预测的停顿时间模型,可以明确指定一个最大停顿时间 (-XX:MaxGCPauseMillis),停顿时间需要不断调优找到一个理想值,过大过小都会拖慢性能。

G1 收集器之所以能建立可预测的停顿时间模型,是因为它可以避免在整个 Java 堆中进行全区域的垃圾收集,G1 根据各个 Region 里垃圾堆积的价值大小(回收所获空间大小及所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region,这也是Garbage-First名称的由来。

G1 收集器的Region如下图所示:

image

图中的 E 代表是 Eden 区,S 代表 Survivor,O 代表 Old 区,H 代表 humongous 表示巨型对象 (大于 Region 空间的对象)。从图中可以看出各个区域逻辑上并不是连续的,并且一个 Region 在某一个时刻是 Eden,在另一个时刻就可能属于老年代。G1 在进行垃圾清理的时候就是将一个 Region 的对象拷贝到另外一个 Region 中。

再介绍一个概念:Remembered Set。每个 Region 中都有一个 Remembered Set,记录的是其他 Region 中的对象引用本 Region 对象的关系(谁引用了我的对象)。所以在垃圾回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏。G1 里面还有另外一种数据结构叫Collection Set,Collection Set 记录的是 GC 要收集的 Region 的集合,Collection Set 里的 Region 可以是任意代的。在 GC 的时候,对于跨代对象引用,只要扫描对应的 Collection Set 中的 Remembered Set 即可。

不算上维护 Remembered Set 的话,G1 收集器的收集过程如下图所示:

image

如图所示,G1 收集过程有如下几个阶段:

  1. 初始标记(Initial Marking)
  2. 并发标记(Concurrent Marking)
  3. 最终标记(Final Marking)
  4. 筛选回收(Live Data Counting and Evacuation)

初始标记只标记一下 GC Roots 能关联到的对象,需要停顿线程但是耗时短,会停顿用户线程(Stop the World)。并发标记是从 GC Root 开始对堆中对象进行可达性分析,找出存活对象,这阶段耗时长但是可以与用户线程并发执行。最终标记就是为了修正在并发标记阶段,因用户线程继续运行而导致标记产生变动的那一部分标记记录,这阶段需要停顿用户线程(Stop the World),但是可并行执行。筛选回收阶段会对各个 Region 的回收价值和成本进行排序,根据用户期望的 GC 停顿时间来制定回收计划,该阶段也是会停顿用户线程(Stop the World)。

垃圾收集参数

VM 参数 描述
-XX:+UseSerialGC 指定 Serial 收集器 +Serial Old 收集器组合执行内存回收
-XX:+UseParNewGC 指定 ParNew 收集器 +Serilal Old 组合执行内存回收
-XX:+UseParallelGC 指定 Parallel 收集器 +Serial Old 收集器组合执行内存回收
-XX:+UseParallelOldGC 指定 Parallel 收集器 +Parallel Old 收集器组合执行内存回收
-XX:+UseConcMarkSweepGC 指定 CMS 收集器 +ParNew 收集器 +Serial Old 收集器组合执行内存回收。优先使用 ParNew 收集器 +CMS 收集器的组合,当出现 ConcurrentMode Fail 或者 Promotion Failed 时,则采用 ParNew 收集器 +Serial Old 收集器的组合
-XX:+UseG1GC 指定 G1 收集器并发、并行执行内存回收
-XX:NewRatio 新生代与老生代 (new/old generation) 的大小比例 (Ratio). 默认值为 2
-XX:SurvivorRatio eden/survivor 空间大小的比例 (Ratio). 默认值为 8
-XX:GCTimeRatio GC 时间占总时间的比率,默认值 99%,仅在 Parallel Scavenge 收集器时生效
-XX:MaxGCPauseMills 设置 GC 最大停顿时间,仅在 Parallel Scavenge 收集器时生效
-XX:PretensureSizeThreshold 直接晋升到老年代的对象大小,大于这个参数的对象直接在老年代分配
-XX:MaxTenuringThreshold 提升年老代的最大临界值 (tenuring threshold). 默认值为 15
-XX:UseAdaptiveSizePolicy 动态调整 Java 堆中各个区域的大小及进入老年代的年龄
-XX:HandlePromotionFailure 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代整个 Eden 和 Survivor 中对象都存活的极端情况
-XX:ParallelGCThreads 设置垃圾收集器在并行阶段使用的线程数, 默认值随 JVM 运行的平台不同而不同
-XX:ParallelCMSThreads 设定 CMS 的线程数量
-XX:ConcGCThreads 并发垃圾收集器使用的线程数量. 默认值随 JVM 运行的平台不同而不同
-XX:CMSInitiatingOccupancyFraction 设置 CMS 收集器在老年代空间被使用多少后触发垃圾收集,默认 68%
-XX:+UseCMSCompactAtFullCollection 设置 CMS 收集器在完成垃圾收集后是否要进行一次内存碎片的整理
-XX:CMSFullGCsBeforeCompaction 设定进行多少次 CMS 垃圾回收后,进行一次内存压缩
-XX:+CMSClassUnloadingEnabled 允许对类元数据进行回收
-XX:CMSInitiatingPermOccupancyFraction 当永久区占用率达到这一百分比时,启动 CMS 回收
-XX:UseCMSInitiatingOccupancyOnly 表示只在到达阀值的时候,才进行 CMS 回收
-XX:G1ReservePercent 设置堆内存保留为假天花板的总量, 以降低提升失败的可能性. 默认值是 10
-XX:G1HeapRegionSize 使用 G1 时 Java 堆会被分为大小统一的的区 (region)。此参数可以指定每个 heap 区的大小. 默认值将根据 heap size 算出最优解. 最小值为 1Mb, 最大值为 32Mb

发表于 2018-04-25,最后编辑于 2018-06-13
本文作者: Cellei
本文链接: http://www.cellei.com/blog/2018/04251
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!