《深入理解 Java 虚拟机》笔记——第3章 垃圾回收器与内存分配策略(二)

本文详细介绍了 Java 虚拟机(JVM)中的各种垃圾收集器,包括 Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS 和 G1。各收集器的特点、工作原理、适用场景以及与其它收集器的配合使用进行了讨论,重点关注 CMS 和 G1 收集器的并发与低停顿特性。文章强调选择适合应用的收集器而非最佳收集器,以及如何通过参数调整来优化性能。
摘要由CSDN通过智能技术生成

声明:

本博客是本人在学习《深入理解 Java 虚拟机》后整理的笔记,旨在方便复习和回顾,并非用作商业用途。

本博客已标明出处,如有侵权请告知,马上删除。

3.5 垃圾收集器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。Java 虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大差别,并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。这里讨论的收集器基于 JDK 1.7 Update 14 之后的 HotSpot 虚拟机(在这个版本中正式提供了商用的 G1 收集器,之前 G1 仍处于实验状态),这个虚拟机包含的所有收集器如图 3-5 所示。

在这里插入图片描述

图 3-5 展示了 7 种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。接下来笔者将逐一介绍这些收集器的特性、基本原理和使用场景,并重点分析 CMS 和 G1 这两款相对复杂的收集器,了解它们的部分运作细节

在介绍这些收集器各自的特性之前,我们先来明确一个观点:虽然我们是在对各个收集器进行比较,但并非为了挑选出一个最好的收集器。因为直到现在为止还没有最好的收集器出现,更加没有万能的收集器,所以我们选择的只是对具体应用最合适的收集器。这点不需要多加解释就能证明:如果有一种放之四海皆准、任何场景下都适用的完美收集器存在,那 HotSpot 虚拟机就没必要实现那么多不同的收集器了。

3.5.1 Serial 收集器

Serial 收集器是最基本、发展历史最悠久的收集器,曾经(在 JDK 1.3.1 之前)是虚拟机新生代收集的唯一选择。大家看名字就会知道,这个收集器是一个单线程的收集器,但它的 “单线程” 的意义并不仅仅说明它只会使用一个 CPU 或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。“Stop The World” 这个名字也许听起来很酷,但这项工作实际上是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户正常工作的线程全部停掉,这对很多应用来说都是难以接受的。读者不妨试想一下,要是你的计算机每运行一个小时就会暂停响应 5 分钟,你会有什么样的心情?

图 3-6 示意了 Serial/Serial Old 收集器的运行过程。

在这里插入图片描述

对于 “Stop The World” 带给用户的不良体验,虚拟机的设计者们表示完全理解,但也表示非常委屈:“你妈妈在给你打扫房间的时候,肯定也会让你老老实实地在椅子上或者房间外待着,如果她一边打扫,你一边乱扔纸屑,这房间还能打扫完?” 这确实是一个合情合理的矛盾,虽然垃圾收集这项工作听起来和打扫房间属于一个性质的,但实际上肯定还要比打扫房间复杂得多啊!

从 JDK 1.3 开始,一直到现在最新的 JDK 1.7,HotSpot 虚拟机开发团队为消除或者减少工作线程因内存回收而导致停顿的努力一直在进行着,从 Serial 收集器到 Parallel 收集器,再到 Concurrent Mark Sweep(CMS)乃至 GC 收集器的最前沿成果 Garbage First(G1)收集器,我们看到了一个个越来越优秀(也越来越复杂)的收集器的出现,用户线程的停顿时间在不断缩短,但是仍然没有办法完全消除(这里暂不包括 RTSJ 中的收集器)。寻找更优秀的垃圾收集器的工作仍在继续!

写到这里,笔者似乎已经把 Serial 收集器描述成一个 “老而无用、食之无味弃之可惜” 的鸡肋了,但实际上到现在为止,它依然是虚拟机运行在 Client 模式下的默认新生代收集器。它也有着优于其他收集器的地方:简单而高效(与其他收集器的单线程比),对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率

在用户的桌面应用场景中,分配给虚拟机管理的内存一般来说不会很大,收集几十兆甚至一两百兆的新生代(仅仅是新生代使用的内存,桌面应用基本上不会再大了),停顿时间完全可以控制在几十毫秒最多一百多毫秒以内,只要不是频繁发生,这点停顿是可以接受的。所以,Serial 收集器对于运行在 Client 模式下的虚拟机来说是一个很好的选择。

3.5.2 ParNew 收集器

ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为都与 Serial 收集器完全一样

ParNew 收集器的工作过程如图 3-7 所示。

在这里插入图片描述

ParNew 收集器除了多线程收集之外,其他与 Serial 收集器相比并没有太多创新之处,但它却是许多运行在 Server 模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的原因是,除了 Serial 收集器外,目前只有它能与 CMS 收集器配合工作

在 JDK 1.5 时期,HotSpot 推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器——CMS 收集器(Concurrent Mark Sweep),这款收集器是 HotSpot 虚拟机中第一款真正意义上的并发(Concurrent)收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。用前面那个例子的话来说,就是做到了在你的妈妈打扫房间的时候你还能一边往地上扔纸屑。

不幸的是,CMS 作为老年代的收集器,却无法与 JDK 1.4.0 中已经存在的新生代收集器 Parallel Scavenge 配合工作,所以在 JDK 1.5 中使用 CMS 来收集老年代的时候,新生代只能选择 ParNew 或者 Serial 收集器中的一个。ParNew 收集器也是使用 -XX:+UseConcMarkSweepGC 选项后的默认新生代收集器,也可以使用 -XX:+UseParNewGC 选项来强制指定它。

ParNew 收集器在单 CPU 的环境中绝对不会有比 Serial 收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个 CPU 的环境中都不能百分之百地保证可以超越 Serial 收集器。当然,随着可以使用的 CPU 的数量的增加,它对于 GC 时系统资源的有效利用还是很有好处的。它默认开启的收集线程数与 CPU 的数量相同,在 CPU 非常多(譬如 32 个,现在 CPU 动辄就 4 核加超线程,服务器超过 32 个逻辑 CPU 的情况越来越多了)的环境下,可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数。

注意

从 ParNew 收集器开始,后面还会接触到几款并发和并行的收集器。在大家可能产生疑惑之前,有必要先解释两个名词:并发和并行。这两个名词都是并发编程中的概念,在谈论垃圾收集器的上下文语境中,它们可以解释如下:

  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个 CPU 上。

3.5.3 Parallel Scavenge 收集器

Parallel Scavenge 收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。看上去和 ParNew 都一样,那它有什么特别之处呢?

Parallel Scavenge 收集器的特点是它的关注点与其他收集器不同,CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量(Throughput)

吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,计算公式如下:

吞吐量 = 运行用户代码时间/(运行用户代码时间+垃圾收集时间)

虚拟机总共运行了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是 99%。

停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务

Parallel Scavenge 收集器的参数

Parallel Scavenge 收集器提供了两个参数用于精确控制吞吐量:

  • 控制最大垃圾收集停顿时间的 -XX:MaxGCPauseMillis 参数

    MaxGCPauseMillis 参数允许的值是一个大于 0 的毫秒数,收集器将尽可能地保证内存回收花费的时间不超过设定值。不过大家不要认为如果把这个参数的值设置得稍小一点就能使得系统的垃圾收集速度变得更快,GC 停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代调小一些,收集 300MB 新生代肯定比收集 500MB 快吧,这也直接导致垃圾收集发生得更频繁一些,原来 10 秒收集一次、每次停顿 100 毫秒,现在变成 5 秒收集一次、每次停顿 70 毫秒。停顿时间的确在下降,但吞吐量也降下来了。

  • 直接设置吞吐量大小的 -XX:GCTimeRatio 参数

    GCTimeRatio 参数的值应当是一个大于 0 且小于 100 的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。如果把此参数设置为 19,那允许的最大 GC 时间就占总时间的 5%(即 1/ (1+19 ) ),默认值为 99,就是允许最大 1%(即 1/(1+99) )的垃圾收集时间。

由于与吞吐量关系密切,Parallel Scavenge 收集器也经常称为 “吞吐量优先” 收集器

除上述两个参数之外,Parallel Scavenge 收集器还有一个参数:

  • GC 自适应的调节策略开关的 -XX:+UseAdaptiveSizePolicy 参数

    这是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为 GC 自适应的调节策略(GC Ergonomics)。如果读者对于收集器运作原来不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成将是一个不错的选择。只需要把基本的内存数据设置好(如 -Xmx 设置最大堆),然后使用 MaxGCPauseMillis 参数(更关注最大停顿时间)或 GCTimeRatio(更关注吞吐量)参数给虚拟机设立一个优化目标,那具体细节参数的调节工作就由虚拟机完成了。自适应调节策略也是 Parallel Scavenge 收集器与 ParNew 收集器的一个重要区别

3.5.4 Serial Old 收集器

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

如果在 Server 模式下,那么它主要还有两大用途:

  1. 一种用途是在 JDK 1.5 以及之前的版本中与 Parallel Scavenge 收集器搭配使用
  2. 另一种用途就是作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用

这两点都将在后面的内容中详细讲解。

Serial Old 收集器的工作过程如图 3-8 所示。

在这里插入图片描述

3.5.5 Parallel Old 收集器

Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程和 “标记-整理” 算法。这个收集器是在 JDK 1.6 中才开始提供的,在此之前,新生代的 Parallel Scavenge 收集器一直处于比较尴尬的状态。原因是,如果新生代选择了 Parallel Scavenge 收集器,老年代除了 Serial Old(PS MarkSweep)收集器外别无选择(还记得上面说过 Parallel Scavenge 收集器无法与 CMS 收集器配合工作吗?)。由于老年代 Serial Old 收集器在服务端应用性能上的 “拖累”,使用了 Parallel Scavenge 收集器也未必能在整体应用上获得吞吐量最大化的效果,由于单线程的老年代收集中无法充分利用服务器多 CPU 的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定有 ParNew 加 CMS 的组合 “给力”。

直到 Parallel Old 收集器出现后,“吞吐量优先” 收集器终于有了比较名副其实的应用组合,在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器

Parallel Old 收集器的工作过程如图 3-9 所示。

在这里插入图片描述

3.5.6 CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的 Java 应用集中在互联网站或者 B/S 系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS 收集器就非常符合这类应用的需求。

CMS 收集器的运作过程

从名字(包含 “Mark Sweep”)上就可以看出,CMS 收集器是基于 “标记——清除” 算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为 4 个步骤,包括:

  • 初始标记(CMS initial mark)

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

  • 并发标记(CMS concurrent mark)

    并发标记阶段就是进行 GC RootsTracing 的过程。

  • 重新标记(CMS remark)

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

  • 并发清除(CMS concurrent sweep)

    并发清除阶段会清除对象,回收所有的垃圾对象。

其中,初始标记、重新标记这两个步骤仍然需要 “Stop The World”。由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的

通过图 3-10 可以比较清楚地看到 CMS 收集器的运作步骤中并发和需要停顿的时间。

在这里插入图片描述

CMS 的缺点

CMS 是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿,Sun 公司的一些官方文档中也称之为并发低停顿收集器(Concurrent Low Pause Collector)。但是 CMS 还远达不到完美的程度,它有以下 3 个明显的缺点:

  • CMS 收集器对 CPU 资源非常敏感。其实,面向并发设计的程序都对 CPU 资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说 CPU 资源)而导致应用程序变慢,总吞吐量会降低。CMS 默认启动的回收线程数是 (CPU 数量+3)/4,也就是当 CPU 在 4 个以上时,并发回收时垃圾收集线程不少于 25% 的 CPU 资源,并且随着 CPU 数量的增加而下降。但是当 CPU 不足 4 个(譬如 2 个)时,CMS 对用户程序的影响就可能变得很大,如果本来 CPU 负载就比较大,还分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了 50%,其实也让人无法接受。为了应付这种情况,虚拟机提供了一种称为 “增量式并发收集器”(Incremental Concurrent Mark Sweep/i-CMS)的 CMS 收集器变种,所做的事情和单 CPU 年代 PC 机操作系统使用抢占式来模拟多任务机制的思想一样,就是在并发标记、清理的时候让 GC 线程、用户线程交替运行,尽量减少 GC 线程的独占资源的时间,这样整个垃圾收集的过程会更长,但对用户程序的影响就会显得少一些,也就是速度下降没有那么明显。实践证明,增量时的 CMS 收集器效果很一般,在目前版本中,i-CMS 已经被声明为 “deprecated”,即不再提倡用户使用。
  • CMS 收集器无法处理浮动垃圾(Floating Garbage),可能出现 “Concurrent Mode Failure” 失败而导致另一次 Full GC 的产生。由于 CMS 并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理掉它们,只好留待下一次 GC 时再清理掉。这一部分垃圾就称为 “浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此 CMS 收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。在 JDK 1.5 的默认设置下,CMS 收集器当老年代使用了 68% 的空间后就会被激活,这是一个偏保守的设置,如果在应用中老年代增长不是太快,可以适当调高参数 -XX:CMSMtiatingOccupancyFraction 的值来提高触发百分比,以便降低内存回收次数从而获取更好的性能,在 JDK 1.6 中,CMS 收集器的启动阈值已经提升至 92%。要是 CMS 运行期间预留的内存无法满足程序需要,就会出现一 次 “Concurrent Mode Failure” 失败,这时虚拟机将启动后备预案:临时启用 Serial Old 收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数 -XX:CM SlnitiatingOccupancyFraction 设置得太高很容易导致大量 “Concurrent Mode Failure” 失败,性能反而降低。
  • 还有最后一个缺点,在本节开头说过,CMS 是一款基于 “标记——清除” 算法实现的收集器,如果读者对前面这种算法介绍还有印象的话,就可能想到这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次 Full GC。为了解决这个问题,CMS 收集器提供了一个 -XX:+UseCMSConpactAtFullCollection 开关参数(默认就是开启的),用于在 CMS 收集器顶不住要进行 FullGC 时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。虚拟机设计者还提供了另外一个参数 -XX:CMSFullGCsBeforeConpaction,这个参数是用于设置执行多少次不压缩的 Full GC 后,跟着来一次带压缩的(默认值为 0,表示每次进入 Full GC 时都进行碎片整理)。

3.5.7 G1 收集器

G1(Garbage-First)收集器是当今收集器技术发展的最前沿成果之一,早在 JDK 1.7 刚刚确立项目目标,Sun 公司给出的 JDK 1.7 RoadMap 里面,它就被视为 JDK 1.7 中 HotSpot 虚拟机的一个重要进化特征。从 JDK 6u14 中开始就有 Early Access 版本的 G1 收集器供开发人员实验、试用,由此开始 G1 收集器的 “Experimental” 状态持续了数年时间,直至 JDK 7u4,Sun 公司才认为它达到足够成熟的商用程度,移除了 “Experimental” 的标识。

G1 收集器的特点

G1 是一款面向服务端应用的垃圾收集器。HotSpot 开发团队赋予它的使命是(在比较长期的)未来可以替换掉 JDK 1.5 中发布的 CMS 收集器

与其他 GC 收集器相比,G1 具备如下特点:

  • 并行与并发:G1 能充分利用多 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿的时间,部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 Java 程序继续执行。
  • 分代收集:与其他收集器一样,分代概念在 G1 中依然得以保留。虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次 GC 的旧对象以获取更好的收集效果。
  • 空间整合:与 CMS 的 “标记——清理” 算法不同,G1 从整体来看是基于 “标记——整理” 算法实现的收集器,从局部(两个 Region 之间)上来看是基于 “复制” 算法实现的,但无论如何,这两种算法都意味着 G1 运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC。
  • 可预测的停顿:这是 G1 相对于 CMS 的另一大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒,这几乎已经是实时 Java(RTSJ)的垃圾收集器的特征了。

G1 实现思路

在 G1 之前的其他收集器进行收集的范围都是整个新生代或者老年代,而 G1 不再是这样。使用 G1 收集器时,Java 堆的内存布局就与其他收集器有很大差别,它将整个 Java 堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分 Region(不需要连续)的集合。

G1 收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集。G1跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region(这就是 Garbage-First 名称的来由)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率

G1 把内存 “化整为零” 的思路,理解起来似乎很容易,但其中的实现细节却远远没有想象中那样简单,否则也不会从 2004 年 Sun 实验室发表第一篇 G1 的论文开始直到今天(将近 10 年时间)才开发出 G1 的商用版。笔者以一个细节为例:把 Java 堆分为多个 Region 后,垃圾收集是否就真的能以 Region 为单位进行

听起来顺理成章,再仔细想想就很容易发现问题:Region 不可能是孤立的。一个对象分配在某个 Region 中,它并非只能被本 Region 中的其他对象引用,而是可以与整个 Java 堆任意的对象发生引用关系。那在做可达性判定确定对象是否存活的时候,岂不是还得扫描整个 Java 堆才能保证准确性?这个问题其实并非在 G1 中才有,只是在 G1 中更加突出而已。在以前的分代收集中,新生代的规模一般都比老年代要小许多,新生代的收集也比老年代要频繁许多,那回收新生代中的对象时也面临相同的问题,如果回收新生代时也不得不同时扫描老年代的话,那么 Minor GC 的效率可能下降不少。

在 G1 收集器中,Region 之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用 Remembered Set 来避免全堆扫描的。G1 中每个 Region 都有一个与之对应的 Remembered Set,虚拟机发现程序在对 Reference 类型的数据进行写操作时,会产生一个 Write Barrier 暂时中断写操作,检查 Reference 引用的对象是否处于不同的 Region 之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过 CardTable 把相关引用信息记录到被引用对象所属的 Region 的 Remembered Set 之中。当进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏。

运作过程

如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:

  • 初始标记(Initial Marking)

    初始标记阶段仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的 Region 中创建新对象,这阶段需要停顿线程,但耗时很短。

  • 并发标记(Concurrent Marking)

    并发标记阶段是从 GC Root 开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。

  • 最终标记(Final Marking)

    最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行。

  • 筛选回收(Live Data Counting and Evacuation)

    筛选回收阶段首先对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划,从 Sun 公司透露出来的信息来看,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

通过图 3-11 可以比较清楚地看到 G1 收集器的运作步骤中并发和需要停顿的阶段。

在这里插入图片描述

3.5.8 理解 GC 日志

每一种收集器的日志形式都是由它们自身的实现所决定的,换而言之,每个收集器的日志格式都可以不一样。但虚拟机设计者为了方便用户阅读,将各个收集器的日志都维持一定的共性,例如以下两段典型的 GC 日志:

33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs]  
100.667: [Full GC [Tenured: 0K->210K(10240K), 0.0149142 secs] 4603K->210K(19456K), [Perm : 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]  
  • 最前面的数字 “33.125:” 和 “100.667:” 代表了 GC 发生的时间,这个数字的含义是从 Java 虚拟机启动以来经过的秒数。

  • GC 日志开头的 "[GC” 和 “[Full GC” 说明了这次垃圾收集的停顿类型,而不是用来区分新生代 GC 还是老年代GC的。如果有 “Full”,说明这次 GC 是发生了 Stop-The-World 的。

  • 接下来的 “[DefNew”、“[Tenured”、“[Perm” 表示 GC 发生的区域,这里显示的区域名称与使用的 GC 收集器是密切相关的。

  • 方括号内部的 “3324K->152K(3712K)” 含义是 “GC 前该内存区域已使用容量-> GC 后该内存区域已使用容量(该内存区域总容量)”。

  • 方括号之外的 “3324K->152K(11904K)” 表示 “GC 前 Java 堆已使用容量-> GC 后 Java 堆已使用容量(Java 堆总容量)”。

  • “0.0025925 secs” 表示该内存区域 GC 所占用的时间,单位是秒。

3.5.9 垃圾收集器参数总结

JDK 1.7 中的各种垃圾收集器到此已全部介绍完毕,在描述过程中提到了很多虚拟机非稳定的运行参数,在表 3-2 中整理了这些参数供读者实践时参考。

在这里插入图片描述

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bm1998

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值