GC 垃圾收集

主要介绍垃圾收集的方法论以及虚拟机根据方法论具体是实现的垃圾收集器。

垃圾收集算法

判断对象已死之后如何把这些对象收集起来并且回收掉,主要的算法有标记-清除复制标记-整理分代收集

标记-清除算法

标记出所有需要回收的对象,标记完成后统一回收。如何标记在可达性分析中有讲。

缺点
  • 效率问题,标记和清除的过程效率都不高
  • 空间问题,清除之后会产生大量的空间碎片,空间碎片过多导致以后在运行过程中如果要给大对象分配内存空间还是空间不足,可能会再次触发 GC。

复制算法

将内存分为两块,其中一块内存用完之后,将还存活的对象复制到另一块,然后将当前内存一次清理掉。这样每次都是半块内存一起回收,不需要考虑内存碎片等。

缺点
  • 只有一半内存在使用,降低了内存的使用率

改进版本是将内存分为 Eden 空间和两块较小的 Survivor 空间,配置比例 $8:1:1$,每次使用 Eden 和一块 Survivor 空间,当需要内存回收时,将还存活对象一次性复制到另一块 Survivor 空间,但是另一块 Survivor 空间不足时需要其他的内存控件来做担保,保证全部存活对象得以复制。

标记-整理算法

复制算法对于存活对象较少的新生代效果较好,如果是老年代内存回收需要转移复制的对象过多可能会降低效率。标记-整理算法是将所有存活的对象向一端移动,然后清理掉边界以外的内存。

分代收集算法

一般对象存活周期将内存分为新生代和老年代,新生代每次垃圾收集时都有大量对象失效,可以选用复制算法;老年代没有其他内存空间作担保、对象存活率也高适用标记-整理算法来回收。

垃圾收集器

垃圾收集算法是垃圾收集的方法论,垃圾收集器是内存回收的具体实现。

Serial 收集器

Serial 收集器是最基本、发展历史最悠久的收集器。

特点
  • 主要针对新生代内存,采用复制算法
  • 单线程收集,必须暂停其他的工作线程
  • 没有线程间的切换开销,获得单线程的最高收集效率,简单而高效
应用
  • 虚拟机在 Client 模式下的默认新生代收集器
配置参数

-XX:+UseSerialGC

是一个“单线程”的收集器,这个“单线程”的意义是当收集器工作时必须暂停其他的工作线程,直到收集结束。这样的优点就是收集器没有线程交互的开销,只做垃圾收集简单而高效。

Serial Old 收集器

Serial 收集器的老年底版本,也是单线程收集,采用标记-整理算法。

应用
  • 在 JDK 1.5 之前版本中与 PS MarkSweep 收集器(类似 SerialOld收集器)搭配使用
  • 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用

ParNew 收集器

Serial 收集器的多线程版本,会有多个收集线程一起工作。如果是在单 CPU 的环境中, ParNew 的效率是不如 Serial 收集器,因为存在线程交互的开销。但如果增加了 CPU 数量 ParNew 的效果会更好。

特点
  • 新生代收集器,采用复制算法
  • 多收集线程并行工作,需要暂停其他用户工作线程
应用
  • 当老年代选择 CMS 收集器后,新生代只能选用 ParNew 收集器或者 Serial 收集器,即老年代配置了 -XX:+UseConcMarkSweepGC 后,ParNew 是默认的新生代收集器。
  • 运行在 Server 模式下的虚拟机的中首选的新生代收集器
配置参数
  • -XX:+UseParNewGC
  • -XX:ParallelGCThreads:限制垃圾收集的线程数

Parallel Scavenge 收集器

特点
  • 新生代收集器,采用复制算法
  • 多线程并行收集
  • 达到可控制的吞吐量(吞吐量=用户运行代码时间 / (用户运行代码时间 + 垃圾收集时间))
应用

高吞吐量可以高效的利用 CPU 时间,尽快完成运算任务,适合在后台运算不需要太多交互的任务

配置参数
  • -XX:+UseParallelGC
  • -XX:MaxGCPauseMillis:最大垃圾收集停顿时间,大于 0 的毫秒数,收集器尽可能的保证收集时间不超过该值
  • -XX:GCTimeRatio:可直接设置吞吐量大小,大于 0 小于 100 的整数,垃圾收集时间占总时间的比率,默认值是 99,即最大允许 1%(1 / (1 + 99))的垃圾回收时间
  • -XX:UseAdaptiveSizePolicy:开关参数,打开后就不需要手动指定新生代(-Xmn)、Eden、Survivor(-XX:SurvivorRatio) 、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数,虚拟机会根据当前系统运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大吞吐量,称为 GC 自适应调节策略。

Parallel Old 收集器

Parallel Scavenge 收集器的老年代版本

特点
  • 使用标记-整理算法,适用于老年代
  • 多线程并行收集
应用

在注重吞吐量以及 CPU 资源敏感的场合,可以选用 Parallel Scavenge + Parallel Old 收集器的组合。

配置参数

-XX:+UseParallelOldGC

CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。适用于老年代。

步骤
  • 初始标记(CMS initial mark):需要暂停用户线程,标记 GC Roots 能直接关联到的对象,速度很快
  • 并发标记(CMS concurrent mark):GC Roots Tracing 的过程,与用户线程一起工作
  • 重新标记(CMS remark):需要暂停用户线程,修正并发标记期间因为用户线程继续运行导致标记产生变动的那一部分记录,停顿时间比初始标记时间稍长,但是比并发标记时间短
  • 并发清除(CMS concurrent sweep):与用户线程一起工作
特点
  • 并发收集、用户线程停顿时间短
  • 使用标记-清除算法
缺点
  • 由于并发收集,用户线程也可以并发执行,所以对 CPU 资源敏感。如果 CPU 资源较少,那收集线程对于用户线程的资源影响就会比较大
  • 无法处理浮动垃圾,在并发清理阶段,用户线程还是继续运行这个阶段产生的垃圾只能等待下一次垃圾回收来处理。因为清理阶段用户线程还在运行,所以 CMS 收集器不是等待老年代快被填满之后再收集,需要留一部分在并发收集时给用户线程使用,可以调节参数 -XX:CMSInitiatingOccupancyFraction 来配置触发百分比,JDK 1.5 默认是 68% 触发收集,JDK 1.6 阈值提升到 92%。如果在 CMS 运行期间,如果预留的内存不满足程序运行需要,会出现 Concurrent Mode Failure 错误,虚拟机会临时启用 Serial Old 收集器来进行老年代回收,这样停顿时间会更长。
  • 由于使用标记-清除算法,会产生大量内存碎片。CMS 提供 -XX:UseCMSCompactAtFullCollection 开关参数(默认开启)在 CMS 收集器要进行 Full GC 时开启内存碎片合并整理过程。内存整理无法并发进行,所以会导致停顿时间变长。可以利用 -XX:CMSFullGCsBeforeCompaction 参数配置在进行过几次无内存整理的 Full GC 后进行一次有压缩整理的 Full GC。
应用
  • CMS 收集器作为老年代收集器时,ParNew 收集器是默认的新生代收集器
  • CMS 的停顿时间短,适合于重视响应速度的互联网或者 B/S 系统的服务端
配置参数

-XX:+UseConcMarkSweepGC

G1 收集器

G1(Garbage First)收集器,注重低延迟,面向服务端应用的垃圾收集器。

特点
  • 并行与并发:利用多 CPU 来缩短停顿时间,通过并发让其他 Java 程序继续执行
  • 分代收集:不需要其他收集器配合可以独立管理内存,采用不同的方式处理新创建对象、存活一段时间对象、经过几次 GC 后依然存活的对象
  • 空间整合:G1 整体上看是采用标记-整理算法实现,从局部(Region之间)是采用复制算法来处理,这样保证 G1 运行期间不会产生内存碎片
  • 可预测的停顿:建立可预测的停顿时间模型,让使用者明确指定一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间的不得超过 N 毫秒
  • 内存分区:G1 采用内存分区的思路将内存划分为大小相等的分区,以区为单位进行回收,将存活的对象复制到另一个空闲分区,可以通过 -XX:G1HeapRegionSize 配置分区的大小(1~32M)
  • 混合收集:G1 的收集也是 Stop-The-World 的,但是年轻代和老年代的界限比较模糊,采用了混合收集的模式,收集时根据分区,不针对是否是老年代或者新生代,可以混合收集
步骤
  • 初始标记(Initial Mark):标记 GC Roots 能直接关联到的对象,并且修改 TAMS(Next Top At Mark Start)的值,让下一阶段用户程序并发运行时能从正确可用 Region 去创建新对象。这个阶段需要暂停用户线程,时间较短
  • 并发标记(Concurrent Mark):从 GC Roots 开始对堆中的对象进行可达性分析,找出存活的对象,耗时较长。可与用户程序并发执行。
  • 最终标记(Final Mark):修正在并发标记期间用户线程继续运行导致标记产生变动的一部分标记记录,需要暂停用户线程但是可以并行执行。
  • 筛选回收(Live Data Counting and Evacuation):首先对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间定制回收计划。用户线程也可并发执行,但是只回收一部分 Region,时间是用户可控,暂停用户线程可以提高收集效率

GC 日志

[Full GC (System.gc()) [Tenured: 71680K->72762K(172032K), 0.0326327 secs] 86171K->72762K(201536K), [Metaspace: 3515K->3515K(1056768K)], 0.0527461 secs] [Times: user=0.02 sys=0.00, real=0.05 secs] [GC (Allocation Failure) [DefNew: 0K->0K(29504K), 0.0013389 secs][Tenured: 72762K->72762K(172032K), 0.0051454 secs] 72762K->72762K(201536K), [Metaspace: 3515K->3515K(1056768K)], 0.0065562 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] [Full GC (Allocation Failure) [TenuredException in thread "main" java.lang.OutOfMemoryError: Java heap space at com.zcyfover.util.gc.GcMainTest.main(GcMainTest.java:30) : 72762K->72742K(172032K), 0.0053641 secs] 72762K->72742K(201536K), [Metaspace: 3515K->3515K(1056768K)], 0.0054030 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]

  • GC、Full GC:表示垃圾收集的停顿类型,有 “Full” 表示发生了 Stop-The-World
  • (System.gc()):表示通过 “System.gc()” 来触发的 GC
  • Tenured、DefNew、Metaspace:表示 GC 发生的内存区域,这个名字由收集器决定,例如还有 PSYoungGen
  • 71680K->72762K(172032K):回收前该区域已使用容量,回收后该区域已使用容量,该内存区域总容量
  • 86171K->72762K(201536K):在方括号之外的表示堆,GC 前堆已使用容量,GC 后堆已使用容量,堆总容量
  • 0.0326327 secs:该内存区域 GC 所占用的时间,单位是秒
  • Times: user=0.02 sys=0.00, real=0.05 secs:分别表示 用户态消耗的 CPU 时间,内核态消耗的 CPU 时间,操作从开始到结束所经过的墙钟时间(Wall Clock Time)

墙钟时间:非运算的等待耗时,例如磁盘等待 I/O、等待线程阻塞时间等

垃圾回收常用参数

参数 描述
UseSerialGC 虚拟机运行在 Client 模式下的默认值,打开此开关后,使用 Serial + SerialOld 收集器组合进行回收
UseParNewGC 打开此开关后使用 ParNew + SerialOld 收集器组合进行回收
UseConcMarkSweepGC 使用 ParNew + CMS + SerialOld 收集器组合进行回收,SerialOld 收集器是当 CMS 出现 Concurrent Mode Failure 失败后的后备收集器
UseParallelGC 虚拟机在 server 模式下的默认值,使用 ParallelScavenge + SerialOld(PS Mark Sweep) 收集器组合进行回收
UseParallelOldGC 使用 ParallelScavenge + ParallelOld 收集器组合收集
UseG1GC 打开开关后,可以独立进行 GC,不需要其他收集器配合
SurvivorRatio 新生代中 Eden 和 Survivor 区域的内存分配比例,默认为 8,Eden : Survivor = 8 : 1
PretenureSizeThreshold 直接晋升到老年代的对象大小,设置这个参数后,需要占用内存大于这个参数的对象直接在老年代中分配内存
MaxTenuringThreshold 晋升到老年代的对象年龄,每个对象在经过一次 Minor GC 后年龄就增加 1,达到这个年龄后就会进入老年代
UseAdaptiveSizePolicy 动态调整 java 堆中各个区域的大小以及进入老年代的年龄
HandlePromotionFailure 是否允许分配担保失败,及老年代的剩余空间不足以应付 Eden 和 Survivor 区域所有存活对象
ParallelGCThreads 设置并行 GC 时进行内存回收的线程数
GCTimeRatio GC 时间占总时间的比率,默认值 99,即允许 1%的 GC 时间,仅在使用 ParallelScavenge 收集器时生效
MaxGCPauseMillis 设置 GC 的最大停顿时间,仅在使用 ParallelScavenge 收集器时生效
CMSInitiatingOccupancyFraction 设置 CMS 收集器在老年代空间被使用多少后触发垃圾收集,默认值为 68%,仅在使用 CMS 收集器时生效
UseCMSCompactAtFullCollection 设置 CMS 收集器在完成垃圾收集后是否要进行一次内存碎片整理,仅在使用 CMS 收集器时生效
CMSFullGCsBeforeCompaction 设置 CMS 收集器在完成若干次垃圾收集器后在启动一次内存碎片整理,,仅在使用 CMS 收集器时生效