垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商,不同版本的 JVM 来实现。
由于 JDK 的版本处于高速迭代过程中,因此 Java 发展至今已经衍生了众多的 GC 版本。
从不同角度分析垃圾收集器,可以将 GC 分为不同的类型。
Java 不同版本的新特性需要关注的点:
按线程数分,可以分为串行垃圾回收器(Serial Collector)和并行垃圾回收器(Parallel Collector)。
串行回收指的是在同一时间段内只允许有一个 CPU 用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束。
在诸如单 CPU 处理器或者较小的应用内存等硬件平台不是特别优越的场合,串行回收器的性能表现可以超过并行回收器和并发回收器。所以串行回收器默认被应用在客户端的 Client 模式下的 JVM 中。
在并发能力比较强的 CPU 上,并行回收器产生的停顿时间要短于串行回收器。
和串行回收相反,并行收集可以运用多个 CPU 同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用了 STW 机制。
按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器。
按碎片处理方式分,可以分为压缩式垃圾回收器和非压缩式垃圾回收器。
按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器。
吞吐量、暂停时间、内存占用,这三者共同构成一个不可能三角。三者总体的表现会随着技术进步而越来越好。一款优秀的收集器通常最多同时满足其中的两项。
这三项里,暂停时间的重要性日益凸显。因为随着硬件发展,内存占用多些越来越能容忍,硬件性能的提升有有助于降低收集器运行时对应用程序的影响,即提高了吞吐量。而内存的扩大,对延迟反而带来负面的效果。
简单来说,主要抓住两点:吞吐量和暂停时间。
吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
这种情况下,应用程序能容忍较高的暂停时间,因此,高吞吐量的应用程序有更长的时间基准,快速响应是不必考虑的。
吞吐量优先,意味着在单位时间内,STW 的时间最短:0.2 + 0.2 = 0.4
暂停时间是指一个时间段内应用程序线程暂停,让 GC 线程执行的状态
暂停时间优先,意味着尽可能让单次 STW 的时间最短:0.1 + 0.1 + 0.1 + 0.1 + 0.1 = 0.5
高吞吐量较好因为这会让应用程序的最终用户感觉到只有应用程序线程在做”生产性“工作。直觉上,吞吐量越高程序运行越快。
低暂停时间(低延迟)较好因为从最终用户的角度来看不管是 GC 还是其他原因导致一个应用被挂起始终是不好的。这取决于应用程序的类型,有时候甚至短暂的 200 毫秒暂停都可能打断终端用户体验。因此具有低的较大暂停时间是非常重要的,特别是对于一个交互式应用程序。
不幸的是 ”高吞吐量“ 和 ”低暂停时间“ 是一对相互竞争的目标(矛盾)。
在设计(或使用)GC 算法时,我们必须确定我们的目标:一个 GC 算法只可能针对两个目标之一(即只专注于较大吞吐量或最小暂停时间),或尝试找到一个二者的折中。
现在标准:在最大吞吐量优先的情况下,降低停顿时间。
有了虚拟机,就一定需要收集垃圾的机制,这就是 Grabage Collection,对应的产品我们称为 Garbage Collector。
串行回收器:Serial、Serial Old
并行回收器:ParNew、Parallel Scavenge、Parallel Old
并发回收器:CMS、G1
新生代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:Serial Old、Parallel Old、CMS
整堆垃圾收集器:G1
两个收集器间有连线,表明它们可以搭配使用:
Serial / Serial Old 、Serial / CMS 、ParNew / Serial Old 、 ParNew / CMS 、Parallel Scavenge / Serial Old 、Parallel Scavenge / Parallel Old 、G1
其中 Serial Old 作为 CMS 出现 ”Concurrent Mode Failure“ 失败的后备预案。
(红色虚线)由于维护和兼容性测试的版本,在 JDK 8 时将 Serial + CMS、ParNew + Serial Old 这两个组合声明为废弃(JEP 173),并在 JDK 9 中完全取消了这些组合的支持(JEP 214),即,移除了这些组合。
(绿色虚线)JDK 14 中:弃用 Parallel Scavenge 和 Serial Old 组合(JEP 366)
(青色虚线)JDK 14中:删除 CMS 垃圾回收器(JEP 363)
-XX:+PrintCommandLineFlags
查看命令行相关参数(包含使用的垃圾收集器)
java -XX:+PrintCommandLineFlags -version
使用命令行指令: jinfo -flag 相关垃圾回收器参数 进程 ID
$ jinfo -flag UseParallelGC 58951 -XX:-UseParallelGC $ jinfo -flag UseParallelOldGC 58951 -XX:-UseParallelOldGC $ jinfo -flag UseG1GC 58951 -XX:-UseG1GC $ jinfo -flag UseConcMarkSweepGC 58951 -XX:+UseConcMarkSweepGC
Serial 收集器是最基本、历史最悠久的垃圾收集器了。JDK 1.3 之前回收新生代唯一的选择。
Serial 收集器作为 HotSpot 中 Client 模式下的默认新生垃圾收集器。
Serial 收集器采用复制算法、串行回收和 STW 机制的方式执行内存回收。
除了年轻代之外,Serial 收集器还提供用于执行老年代垃圾收集的 Serial Old 收集器。Serial Old 收集器同样也采用了串行回收和 STW 机制,只不过内存回收算法使用的是标记-压缩算法。
这个收集器是一个单线程的收集器,但它的 ”单线程“ 的意义并不仅仅说明它只会使用一个 CPU 或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
优势:简单而高效(与其他收集器的单线程比),对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
在用户的桌面应用场景中,可用内存一般不大(几十 MB 至一两百 MB),可以在较短时间内完成垃圾回收(几十 ms 至一百多 ms),只要不频繁发生,使用串行回收器是可以接受的。
在 HotSpot 虚拟机中,使用 -XX:UseSerialGC
参数可以指定年轻代和老年代都是用串行收集器。
总结:这种垃圾收集器大家了解,现在已经不用串行的了。而且在限定单核 CPU 才可以用。现在都不是单核的了。
对于交互较强的应用而言,这种垃圾收集器是不能接受的。一般在 Java Web 应用程序中是不会采用串行垃圾收集器的。
如果说 Serial GC 是年轻代中的单线程垃圾收集器,那么 ParNew 收集器则是 Serial 收集器的多线程版本。
ParNew 收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。ParNew 收集器在年轻代中同样也是采用复制算法、STW 机制。
ParNew 是很多 JVM 运行在 Server 模式下新生代的默认垃圾收集器。
由于 ParNew 收集器是基于并行回收,那么是否可以断定 ParNew 收集器的回收效率在任何场景下都会比 Serial 收集器更高效?
除 Serial 外,目前只有 ParNew GC 能与 CMS 收集器配合工作。
在程序中,开发人员可以通过选项 -XX:+UseParNewGC
手动指定使用 ParNew 收集器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代。
-XX:ParallelGCThreads
限制线程数量,默认开启和 CPU 数相同的线程数。
HotSpot 的年轻代中除了拥有 ParNew 收集器是基于并行回收以外,Parallel Scavenge 收集器同样也采用了复制算法、并行回收和 STW 机制。
那么 Parallel 收集器的出现是否多此一举?
高吞吐量则可以高效地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。因此,常见在服务器环境中使用。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序。
Parallel 收集器在 JDK 1.6 时提供了用于执行老年代垃圾收集的 Parallel Old 收集器,用来代替老年代的 Serial Old 收集器。
Parallel Old 收集器采用了标记-压缩算法,但同样也是基于并行回收和 STW 机制。
在吞吐量优先的应用场景中,Parallel 收集器和 Parallel Old 收集器的组合,在 Server 模式下的内存回收性能很不错。
在 Java 8 中,默认也是此垃圾收集器。
-XX:+UserParallelGC
手动指定年轻代使用 Parallel 并行收集器执行内存回收任务。-XX:+UseParallelOld
手动指定老年代都是使用并行回收收集器。
-XX:ParallelGCTheads
设置年轻代并行收集器的线程数。一般地,最好与 CPU 数量相等,以避免过多的线程数影响垃圾收集性能。
3 + ((5 * CPU_COUNT) / 8 )
。-XX:MaxGCPauseMillis
设置垃圾收集器最大停顿时间(即 STW 的时间)。单位是毫秒。
-XX:GCTimeRatio
垃圾收集时间占总时间的比例(= 1 / (N + 1))。用于衡量吞吐量的大小。
-XX:MaxGCPauseMillis
参数有一定的矛盾性。暂停时间越长,Radio 参数就容易超过设定的比例。-XX:+UseAdaptiveSizePolicy
设置 Parallel Scavenge 收集器具有自适应调节策略。
在 JDK 1.5 时期,HotSpot 推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器:CMS(Concurrent-Mark-Sweep)收集器,这款收集器是 HotSpot 虚拟中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。
CMS 收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。
CMS 的垃圾收集算法采用 标记-清除 算法,并且也会 STW。
不幸的是,CMS 作为老年代的收集器,却无法与 JDK 1.4.0 中已经存在的新生代收集器 Parallel Scavenge 收集器配合工作,所以在 JDK 1.5 中使用 CMS 来收集老年代的时候,新生代只能选择 ParNew 或者 Serial 收集器中的一个。
在 G1 出现之前,CMS 使用还是非常广泛的,一直到今天,任然有很多系统使用 CMS GC。
CMS 整个过程比之前的收集器要复杂,整个过程分为 4 个主要阶段,即初始标记阶段、并发标记阶段、重新标记阶段、并发清除阶段。
尽管 CMS 收集器采用的是并发回收(非独占式),但是在其初始化标记和再次标记这两个阶段中仍然需要执行 STW 机制暂停程序中的工作线程,不过暂停时间并不会太长,因此可以说明目前所有的垃圾收集器都做不到完全不需要 STW,只是尽可能的缩短暂停时间。
由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低延迟的。
另外,由于在垃圾收集阶段用户线程没有中断,所以在 CMS 回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此,CMS 收集器不能像其他收集器那样等到老年代几乎完全被填满了在进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在 CMS 工作过程中依然有足够的内存空间支持应用程序运行。要是 CMS 运行期间预留的内存无法满足程序需要,就会出现一次 ”Concurrent Mode Failure“ 失败,这时虚拟机将启动后备预案:临时启用 Serial Old 收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
CMS 收集器的垃圾收集算法采用的是 标记-清除 算法,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块,不可避免地将会产生一些内存碎片。那么 CMS 在为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能选择空闲列表(Free List)执行内存分配。
有人会觉得既然 Mark Sweep 会造成内存碎片,那么为什么不把算法换成 Mark Compact 呢?
答案其实很简单,因为当并发清除的时候,用 Compact 整理内存的话,原来的用户线程使用的内存还怎么用?要保证用户线程能继续执行,前提得是它运行的资源不受影响。Mark Compact 更适合 STW 这种场景下使用。
-XX:+UseConcMarkSweepGC
手动指定使用 CMS 收集器执行内存回收任务。
-XX:+UseParNewGC
打开。即:年轻代使用 ParNew 收集器 + 老年代使用 CMS 收集器 + 老年代的备用收集器 Serial Old 收集器-XX:CMSInitiatingOccupanyFraction
设置堆内存使用率的阈值,一旦到达该阈值,便开始进行回收。
-XX:+UseCMSCompactAtFullCollection
用于指定在执行完 Full GC 后对内存空间进行压缩整理,以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了。-XX:CMSFullGCsBeforeCompaction
设置在执行多少次 Full GC 后对内存空间进行压缩整理。-XX:ParallelCMSThreads
设置 CMS 的线程数量。
HotSpot 有这么多的垃圾回收器,那么如果有人问,Serial GC、Parallel GC、CMS GC 这三个 GC 有什么不同呢?
JDK 9 新特性:CMS 被标记为 Deprecate 了(JEP291)
-XX:+UseConcMarkSweepGC
来开启 CMS 收集器的话,用户会收到一个警告信息,提示 CMS 为了将会被去除。JDK 14 新特性:去除 CMS 垃圾收集器(JEP363)
-XX:+UseConcMarkSweepGC
的话,JVM 不会报错,只是给出警告,但是不会退出。JVM 会自动使用默认的 GC。1.既然我们已经有了前面几个强大的 GC,为什么还要发布 Garbage First(G1)GC?
原因就在于对于应用程序所应用的业务越来越庞大、复杂,用户越来越多,没有 GC 就不能保证应用程序正常进行,而经常造成 STW 的 GC 又跟不上实际的需求,所以才会不断地尝试对 GC 进行优化。G1(Garbage First)垃圾回收器是在 Java 7 Update 4 之后引入的一个新的垃圾回收器,是当今收集器技术发展的最前沿成果之一。
与此同时,为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间(pause time),同时兼顾良好的吞吐量。
官方给 G1 设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才担当起”全功能收集器“的重任与期望。
2.为什么名字叫做 Garbage First(G1)呢?
因为 G1 是一个并行回收器,它把堆内存分割成很多不相关的区域(Region)(物理上不连续的)。使用不同的 Region 来表示 Eden、Survivor0、Survivor1、老年代等。
G1 GC 有计划地避免在整个 Java 堆中进行全区域的垃圾收集。G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
由于这种方式的侧重点在于回收垃圾最大量的区间(Region),所以我们给 G1 一个名字:垃圾优先(Garbage First)。
G1(Garbage First)是一款面向服务端应用的垃圾收集器,主要针对配备多核 CPU 及大容量内存的机器,以极高概率满足 GC 停顿时间的同时,还兼具高吞吐量的性能特征。
在 JDK 1.7 版本正式启用,移除了 Experimental 的标识,是 JDK 9 以后的默认垃圾回收器,取代了 CMS 回收器以及 Parallel + Parallel Old 组合。被 Oracle 官方称为 ”全功能的垃圾收集器“。
与此同时,CMS 已经在 JDK 9 中被标记为废弃(Deprecated)。在 JDK 8 中还不是默认的垃圾回收器,需要使用 -XX:+UseG1GC
来使用。
与其他的 GC 收集相比,G1 使用了全新的分区算法,其特定如下所示:
相较于 CMS,G1 还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1 无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(Overload)都要比 CMS 要高。
从经验上说,在小内存应用上 CMS 的表现大概率会优于 G1,而 G1 在大内存应用上则发挥其优势。平衡点在 6-8 GB 之间。
-XX:+UseG1GC
手动指定使用 G1 收集器执行内存回收任务。JDK 9 及以后默认开启。-XX:G1HeapRegionSize
设置每个 Region 的大小。值是 2 的幂,范围是 1 MB 到 32 MB 之间,目标是根据最小的 Java 堆大小划分出约 2048 个区域。默认是堆内存的 1/2000。-XX:MaxGCPauseMillis
设置期望达到的最大 GC 停顿时间指标(JJVM 会尽力实现,但不保证达到)。默认值是 200 ms。-XX:ParallelGCThread
设置 STW 工作线程数的值。最多设置为 8。-XX:ConcGCThreads
设置并发标记的线程数。将 n 设置为并行垃圾回收线程数(ParallelGCThreads)的 1/4 左右。-XX:InitiatingHeapOccupanyPercent
设置触发并发 GC 周期的 Java 堆占用率阈值。超过此值,就触发 GC。默认值是 45。G1 的设计原则就是简化 JVM 性能调优,开发人员只需要简单的三步即可完成调优:
G1 中提供了三种垃圾回收模式:Young GC、Mixed GC 和 Full GC,在不同的条件下被触发。
使用 G1 收集器时,它将整个 Java 堆划分成约 2048 个大小相同的独立 Region 块,每个 Region 块大小根据堆空间的实际大小而定,整体被控制在 1MB 到 32 MB 之间,且为 2 的 N 次幂,即 1 MB,2 MB,4 MB,8 MB,16 MB,32 MB。可以通过 -XX:G1HeapRegionSize
设定。所有的 Region 大小相同,且在 JVM 生命周期内不会被改变。
虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分 Region(不需要连续)的集合。通过 Region 的动态分配方式实现逻辑上的连续。
一个 Region 有可能属于 Eden、Survivor 或者 Old/Tenured 内存区域。但是一个 Region 只可能属于一个角色。图中的 E 表示该 Region 属于 Eden 内存区域,S 表示属于 Survivor 内存区域,O 表示属于 Old 内存区域。图中空白的表示未使用的内存空间。
G1 垃圾收集器还增加了一种新的内存区域,叫做 Humongous 内存区域,如图中的 H 块。主要用于存储大对象,如果超过一个 Region 的50%,就放到 H。(这里老师讲解有误,在《JVM G1源码分析和调优》书中写到:对于大对象分为两类,一类是大于HeapRegionSize的一半,但是小于HeapRegionSize,即一个完整的堆分区可以保存,则直接从空闲列表直接拿一个堆分区,或者分配一个新的堆分区。如果是连续对象,则需要多个堆分区,思路同上,但是处理的时候需要加锁。)
设置 H 的原因:
对于堆中的大对象,默认直接会分配到老年代,但是如果它是一个短期存在的大对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1 划分了一个 Humongous 区,它用来专门存放大对象。**如果一个 H 区装不下一个大对象,那么 G1 会寻找连续的 H 区来存储。**为了能找到连续的 H 区,有时候不得不启用 Full GC。G1 的大多数行为都把 H 区作为老年代的一部分来看待。
G1 GC 的垃圾回收过程主要包括如下三个环节:
Young GC → Young GC + Concurrent Marking → Mixed GC → Full GC
应用程序分配内存,当年轻代的 Eden 区用尽时开始年轻代回收过程;G1 年轻代收集阶段是一个并行的独占式收集器。在年轻代回收期,G1 GC 暂停所有应用程序线程,启动所线程执行年轻代回收。然后从年轻代区间移动存活对象到 Survivor 区间或者老年代区间,也有可能是两个区间都会涉及。
当堆内存使用达到一定值(默认 45%)时,开始老年代并发标记过程。
标记完成马上开始混合回收过程。对于一个混合回收期,G1 GC 从老年区间移动存活对象到空闲区间,这些空闲区间也就成为了老年代的一部分。和年轻代不同,老年代的 G1 回收器和其他 GC 不同**,G1 的老年代回收器不需要整个老年代被回收,一次只需要扫描/回收一小部分老年代的 Region 就可以了**。同时,这个老年代 Region 是和年轻代一起被回收的。
举个例子:一个 Web 服务器,Java 进程最大堆内存为 4 G,每分钟响应 1500 个请求,每 45 秒钟会新分配大约 2 G 内存。G1 会每 45 秒钟进行一次年轻代回收,每 31 个小时整个堆的使用率会达到 45 %,会开始老年代并发标记过程,标记完成后开始四到五次的混合回收。
解决方法:
JVM 启动时,G1 先准备好 Eden 区,程序在运行过程中不断创建对象到 Eden 区,当 Eden 空间耗尽时,G1 会启动一次年轻代垃圾回收过程。
年轻代垃圾回收只会收集 Eden 区和 Survivor 区。
YGC 时,首先 G1 停止应用程序的执行(STW),G1 创建回收集(Collection Set),回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含年轻代 Eden 区和 Survivor 区所有的内存分段。
然后开始如下回收过程:
第一阶段,扫描根。
根是指 static 变量指向的对象,正在执行的方法调用链上的局部变量等。根引用连同 RSet 记录的外部引用作为扫描存活对象的入口。
第二阶段,更新 RSet。
处理 Dirty Card Queue 中的 card,更新 RSet。此阶段完成后,RSet 可以准确的反映老年代对所在的内存分段中对象的引用。
第三阶段:处理 RSet。
识别被老年代对象所指向的 Eden 中的对象,这些被指向的 Eden 中的对象被认为是存活的对象。
第四阶段:复制对象。
此阶段,对象树被遍历,Eden 区内存段中存活的对象会被复制到 Survivor 区中空的内存分段,Survivor 区内存段中存活的对象如果年龄未达到阈值,年龄会加 1,达到阈值会被复制到 Old 区中的内存分段,如果 Survivor 空间不够,Eden 空间的部分数据会直接晋升到老年代空间。
第五阶段:处理引用。
处理 Soft、Weak、Phantom、Final、JNI Weak 等引用(这里可能描述不准确)。最终 Eden 空间的数据为空,GC 停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。
注:对于应用程序的引用赋值语句 object.fieled = object,JVM 会在之前和之后执行特殊的操作以在 Dirty Card Queue 中入队一个保存了对象引用信息的 card,在年轻代回收的时候,G1 会对 Dirty Card Queue 中所有的 card 进行处理,以更新 RSet,保证 RSet 实时准确的反映引用关系。
那为什么不在引用赋值语句处直接更新 RSet 呢?这是为了性能的需要,RSet 的处理需要线程同步,开销会很大,使用队列性能会好很多。
当越来越多的对象晋升到老年代 Old Region 时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即 Mixed GC,该算法并不是一个 Old GC,除了回收整个 Young Region,还会回收一部分的 Old Region。这里需要注意:是一部分老年代,而不是全部老年代。可以选择哪些 Old Region 进行收集,从而可以对垃圾回收的耗时时间进行控制。也要注意的是 Mixed GC 并不是 Full GC。
-XX:G1MixedGCCountTarget
设置)被回收。-XX:G1MixedGCLiveThresholdPercent
,默认为 65%,意思是垃圾占内存分段比例要达到 65% 才会被回收。如果垃圾占比太低,意味着存活的对象占比高,在复制的时候会花费更多的时间。-XX:G1HeapWastePercent
,默认值为 10%,意思是允许整个堆内存中有 10% 的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于 10%,则不再进行混合回收。因为 GC 会花费很多的时间但是回收到的内存却很少。G1的初衷就是要避免 Full GC 的出现。但是如果上述方式不能正常工作,G1 会停止应用程序的执行(Stop-The-World),使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长。
要避免 Full GC 的发生,一旦发生 Full GC,需要对JVM参数进行调整。什么时候会发生 Full GC 呢?比如堆内存太小,当 G1 在复制存活对象的时候没有空的内存分段可用,则会回退到 Full GC,这种情况可以通过增大内存解决。
导致 G1 Full GC 的原因可能有两个:
从 Oracle 官方透露出来的信息可获知,回收阶段(Evacuation)其实本也有想过设计成与用户程序一起并发执行,但这件事情做起来比较复杂,考虑到 G1 只是回一部分 Region,停顿时间是用户可控制的,所以并不迫切去实现,**而选择把这个特性放到了 G1 之后出现的低延迟垃圾收集器(即 ZGC)中。**另外,还考虑到 G1 不是仅仅面向低延迟,停顿用户线程能够最大幅度提高垃圾收集效率,为了保证吞吐量所以才选择了完全暂停用户线程的实现方案。
截止 JDK 1.8,一共有 7 款不同的垃圾收集器。每一款的垃圾收集器都有不同的特点,在具体使用的时候,需要根据具体的情况选用不同的垃圾收集器。
垃圾收集器 | 分类 | 作用位置 | 使用算法 | 特点 | 适用场景 |
---|---|---|---|---|---|
Serial | 串行运行 | 新生代 | 复制算法 | 响应速度优先 | 适用于单 CPU 环境下的 Client 模式 |
ParNew | 并行运行 | 新生代 | 复制算法 | 响应速度优先 | 多 CPU 环境 Server 模式下与 CMS 配合使用 |
Parallel | 并行运行 | 新生代 | 复制算法 | 吞吐量优先 | 适用于后台运算而不需要太多交互的场景 |
Serial Old | 串行运行 | 老年代 | 标记-压缩算法 | 响应速度优先 | 适用于单 CPU 环境下的 Client 模式 |
Parallel Old | 并行运行 | 老年代 | 标记-压缩算法 | 吞吐量优先 | 适用于后台运算而不需要太多交互的场景 |
CMS | 并发运行 | 老年代 | 标记-清除算法 | 响应速度优先 | 适用于互联网或 B/S 业务 |
G1 | 并发、并行运行 | 新生代、老年代 | 标记-压缩算法、复制算法 | 响应速度优先 | 面向服务端应用 |
GC 发展阶段
Serial → Parallel → CMS → G1 → ZGC
Java 垃圾收集器的配置对于 JVM 优化来说是一个很重要的选择,选择合适的垃圾收集器可以让 JVM 的性能有一个很大的提升。怎么选择垃圾收集器?
最后需要明确一个观点:
通过阅读GC日志,我们可以了解Java虚拟机内存分配与回收策略。
内存分配与垃圾回收的参数列表
GCViewer、GCEasy、GCHisto、GCLogViewer、Hpjmeter、garbagecat 等
GC 仍然处于飞速发展之中,目前的默认选项 G1 GC 在不断的进行改进,很多我们原来认为的缺点,例如串行的 Full GC、Card Table 扫描的低效等,都已经被大幅改进,例如,JDK10 以后,Full GC 已经是并行运行,在很多场景下,其表现还略优于 Parallel GC 的并行 Full GC 实现。
即使是 Serial GC,虽然比较古老,但是简单的设计和实现未必就是过时的,它本身的开销,不管是GC相关数据结构的开销,还是线程的开销,都是非常小的,所以随着云计算的兴起,在 Serverless 等新的应用场景下,Serial GC 找到了新的舞台。
比较不幸的是 CMS GC,因为其算法的理论缺陷等原因,虽然现在还有非常大的用户群体,但在 JDK 9 中已经被标记为废弃,并在 JDK 14 版本中移除。
现在 G1 回收器已成为默认回收器好几年了。我们还看到了引入了两个新的收集器:ZGC(JDK 11出现)和 Shenandoah(Open JDK 12),其特点:主打低停顿时间。
Open JDK12的Shenandoash GC:低停顿时间的GC(实验性)
Shenandoah 无疑是众多 GC 中最孤独的一个。是第一款不由 Oracle 公司团队领导开发的 Hotspot 垃圾收集器。不可避免的受到官方的排挤。比如号称 openJDK 和 OracleJDK 没有区别的 Oracle 公司仍拒绝在 Oracle JDK12 中支持 Shenandoah。
Shenandoah 垃圾回收器最初由 RedHat 进行的一项垃圾收集器研究项目 Pauseless GC 的实现,旨在针对 JVM 上的内存回收实现低停顿的需求。在 2014 年贡献给 OpenJDK。
Red Hat 研发 Shenandoah 团队对外宣称,Shenandoah 垃圾回收器的暂停时间与堆大小无关,这意味着无论将堆设置为 200MB 还是200GB,99.9% 的目标都可以把垃圾收集的停顿时间限制在十毫秒以内。不过实际使用性能将取决于实际工作堆的大小和工作负载。
这是 RedHat 在 2016 年发表的论文数据,测试内容是使用 ES 对 200GB 的维基百科数据进行索引。从结果看:
总结
相关解读:尚硅谷宋红康Java12&13新特性教程(深入解读java12&13)
https://www.bilibili.com/video/BV1jJ411M7kQ
官方文档:https://docs.oracle.com/en/java/javase/12/gctuning/
ZGC 与 Shenandoah 目标高度相似,在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停颇时间限制在十毫秒以内的低延迟。
《深入理解Java虚拟机》一书中这样定义 ZGC:ZGC 收集器是一款基于 Region 内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-压缩算法的,以低延迟为首要目标的一款垃圾收集器。
ZGC 的工作过程可以分为 4 个阶段:并发标记 - 并发预备重分配 - 并发重分配 - 并发重映射 等。
ZGC 几乎在所有地方并发执行的,除了初始标记的是 STW 的。所以停顿时间几乎就耗费在初始标记上,这部分的实际时间是非常少的。
在 ZGC 的强项停顿时间测试上,它毫不留情的将 Parallel、G1 拉开了两个数量级的差距。无论平均停顿、95% 停顿、99.8% 停顿、99. 98% 停顿,还是最大停顿时间,ZGC 都能毫不费劲控制在 10 毫秒以内。
虽然 ZGC 还在试验状态,没有完成所有特性,但此时性能已经相当亮眼,用“令人震惊、革命性”来形容,不为过。未来将在服务端、大内存、低延迟应用的首选垃圾收集器。
AliGC 是阿里巴巴 JVM 团队基于 G1 算法,面向大堆(LargeHeap)应用场景。指定场景下的对比: