GC分类

按线程数:分为串行垃圾回收器和并行垃圾回收器

按工作模式:分为并发式垃圾回收器和独占式垃圾回收器

按碎片处理方式:分为

1、压缩式垃圾回收器:在回收完成后,对存活对象进行压缩整理,清除回收后的碎片(再分配对象空间的使用:指针碰撞)

2、非压缩式垃圾回收器(再分配对象空间的使用:空闲列表)

按工作的内存区间:分为年轻代垃圾回收器和老年代垃圾回收器


GC性能指标

吞吐量:运行用户代码·的时间占总运行时间的比例(总运行时间 = 程序运行时间 + 内存回收时间)

吞吐量优先,意味着在单位时间内,STW的时间最短

垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例

暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间(STW)

暂停时间优先,意味着尽可能让单次STW的时间最短

收集频率:相对于应用程序的执行,收集操作发生的频率

内存占用:Java堆区所占的内存大小

快速:一个对象从诞生到被回收所经历的时间

注意:现在的标准为在最大吞吐量优先的情况下,降低停顿时间


回收器概述

新生代收集器:Serial GC、ParNew GC、Parallel Scavenge GC
老年代收集器:Serial 0ld GC、 Parallel 0ld GC、 CMS GC
整堆收集器:G1 GC


GC发展阶段:
Seria1=>Para11e1(并行)=>CMS(并发)=>G1=>ZGC

查看命令行相关参数(包含使用的垃圾收集器):-xx:+PrintCommandLineFlags


Serial,SerialOld 回收器

Serial:串行

HotSpot虚拟机Client模式下的默认新生代垃圾收集器(最基本,最悠久)

采用复制算法,串行回收和STW机制方法执行内存回收

优势:简单,高效(没与其他收集器的单线程相比,没有线程交互的开销)


Serial Old:串行

Serial 0ld是运行在Client模式下默认的老年代垃圾回收器

Serial 0ld收集器同样采用了串行回收和STW机制,只不过内存回收算法使用的是标记一压缩算法

在Server模式下的用途:

1、与新生代的Parallel Scavenge配合使用

2、作为老年代CMS收集器的后备垃圾收集方法

单线程回收:使用一个cpu或一条线程去完成垃圾收集工作而且必须暂停其他所有的工作线程

设置Serial垃圾回收器

使用 -XX:+UseSerialGC 参数可以指定年轻代和老年代都使用串行收集器,等价于新生代用Serial GC,且老年代用Serial 0ld GC


Parallel New:并行

ParNew收集器是Serial收集器的多线程版本,除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。ParNew收集器在年轻代中同样也是采用复制算法、STW机制目前只有ParNew GC能与CMS收集器配合工作

设置ParNew垃圾回收器

-XX:+UseParNewGC:指定使用ParNew收集器执行内存回收任务。它表示年轻代使用并行收集器, 不影响老年代

-XX:ParallelGCThreads限制线程数量,默认开启和CPU数据相同的线程数


Parallel Scavenge/ParallelOld:吞吐量优先

Parallel

HotSpot的年轻代中除了拥有ParNew收集器是基于并行回收的以外,还用到Parallel Scavenge。该收集器同样也 采用了复制算法、并行回收和STW机制。在Java8中,默认是此垃圾收集器

Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量,自适应调节策略是Parallel Scavenge与ParNew一个重要区别

高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。常见在服务器环境中使用。例如执行批量处理、订单处理、工资支付、科学计算


Parallel 0ld

Parallel 0ld收集器采用了标记一压缩算法,但同样也是基于并行回收和STW机制

Parallel收集器在JDK1.6时提供了用于执行老年代垃圾收集的 Parallel 0ld收集器,用来代替老年代的Serial 0ld收集器


指令

-XX:+UseParallelGC:手动指定年轻代使用Parallel并行收集器执行内存回收任务

-XX:+UseParallelOldGC:手动指定老年代都是使用并行回收收集器

默认jdk8是开启的。上面两个参数,默认开启一个,另一个也会被开启。

-XX:ParallelGCThreads设置年轻代并行收集器的线程数,一般最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能

-XX:MaxGCPauseMillis:设置垃圾收集器最大停顿时间(即STW的时间)

-XX:+UseAdaptiveSizePolicy:设置Parallel Scavenge收集器具有自适应调节策略

在这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,已达到在堆大小、吞吐量和停顿时间之间的平衡点
在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量和停顿时间,让虚拟机自己完成调优工作)


CMS:低延迟

HotSpot虚拟机中第一款真正意义上的并发收集器。第一次实现了让垃圾收集线程与用户线程同时工作 也采用标记-清除算法和STW机制。在G1出现之前,CMS使用还是非常广泛的


初始标记

标记出和GCRoots能直接关联到的对象,有STW现象(暂时时间非常短)


并发标记阶段

从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不用STW。**可以与垃圾收集线程一起并发运行


重新标记阶段

有些对象可能开始是垃圾,在并发标记阶段,由于用户线程的影响,导致不是垃圾了,这里需要重新标记的是这部分对象,这个阶段的STW通常会比初始标记阶段稍长一些,但远比并发标记阶段的时间短

注意:重新标记是对已标记过的垃圾进行检测,此时会产生浮动垃圾


并发清除

清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。 由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的


总结

在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。

要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial 0ld收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。

CMS收集器的垃圾收集算法采用的是标记一清除算法,这意味着不可避免地将会产生一些内存碎片。 那么CMS在为新对象分配内存空间时,将无法使用指针碰撞,而只能够选择空闲列表执行内存分配。


最小化使用内存和并行开销:Serial GC(young)+ Serial Old GC(old)

最大化应用程序吞吐量:Parallel GC(young)+ Parallel Old GC(old)

最小化GC中断或停顿时间:Parallel New GC(young)+ CMS GC(old)


优点:

1)低延迟(最耗费时间的并发标记和并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的

2)并发收集

缺点

1)产生内存碎片

2)CMS收集器对CPU资源很敏感(并发阶段,占用一部分线程,吞吐量降低

3)CMS收集器无法处理浮动垃圾

在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间


参数设置

-XX:+UseConcMarkSweepGC:手动指定使用CMS收集器执行内存回收任务(老年代)
(开启该参数后会自动将-XX:+UseParNewGc打开。即: ParNew (Young区用) +CMS (0ld区用) +Serial 0ld的组合)

-XX:CMSlnitiatingOccupanyFraction:设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收

JDK5以前默认为68,即当老年代的空间使用率达到68%时,会执行一次CMS 回收。JDK6及以上默认为92%

通过该选项便可以有效降低Full GC的执行次数

-XX:+UseCMSCompactAtFullCollection:用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生

-XX:CMSFullGCsBeforeCompaction:设置在执行多少次Full GC后对内存空间进行压缩整理

-XX:ParallelCMSThreads:设置CMS的线程数量


G1:区域化分代式

并行与并发

并行性:G1在回收期间,可以有多个GC线程同时工作(此时用户线程STW)

并发性:G1拥有与应用程序交替执行的能力, 部分工作可以和应用程序同时执行,因此,不会在整个回收阶段发生完全阻塞应用程序的情况


分代收集

G1仍然属于分代型垃圾回收器。它区分年轻代和老年代,年轻代依然有Eden区和Survivor区。但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再有固定大小和固定数量

将堆空间分为若干个区域(region),这些区域中包含了逻辑上的年轻代和老年代。


空间整合

G1将内存划分为一个个region,内存的回收以region为单位,region之间是复制算法。整体上可看作标记-压缩算法,两种算法都可以避免内存碎片。而且当分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。


可预测的停顿时间模型

软实时(soft real-time)

G1能建立可预测的停顿时间模型,让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒,可以通过参数-XX:MaxGCPauseMillis进行设置

由于分区的原因,G1可以只选取部分区域进行内存回收,缩小了回收的范围,对于全局停顿情况的发生也能得到较好的控制。

G1 跟踪各个Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

注意:在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间


参数设置

-XX:+UseG1GC:手动指定使用G1收集器执行内存回收任务

-XX:G1HeapRegionSize:设置每个Region的大小。值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000

-XX:MaxGCPauseMillis设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到)默认200ms**(如果这个值设置很小,如20ms,那么它收集的region会少,这样长时间后,堆内存会满。产生FullGC从而出现STW)**

-XX:ParallelGCThread:设置STW时GC线程数的值。最多设置为8(垃圾回收线程)

-XX:ConcGCThreads:设置并发标记的线程数。将n设置为并行垃圾回收线程数的1/4左右

-XX:InitiatingHeapOccupancyPercent:设置触发并发GC周期的Java堆占用率阈值。超过此值,就触发GC(默认45)

  1. 开启G1垃圾收集器
  2. 设置堆的最大内存
  3. 设置最大的停顿时间

region

1、使用G1收集器时,它将整个Java堆划分成约2048个大小相同的独立Region块,每个Region块大小为2的N次幂。可以通过-XX:G1HeapRegionSize设定。所有的Region大小相同,且在JVM生命周期内不会被改变

2、一个region 有可能属于任何一块内存区域。但是一个region只可能属于一个角色。

3、Humongous内存区域(图中的H块)主要用于存储大对象,如果超过1. 5个region,就放到Humongous区

对于堆中的大对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放大对象。如果一个H区装不下一个大对象,那么G1会寻找连续的H区来存储。为了能找到连续的H区,可以启动Full GC。G1的大多数行为都把H区作为老年代的一部分来看待


记忆集

一个region中的对象可能被其他任意region中对象引用,因此在判断对象是否存活的时候,需要扫描整个堆才能保证准确(出现STW)。所以使用每一个region对应一个记忆集(Remembered Set)来避免全局扫描

每次Reference类型数据写操作时,都会产生一个写屏障(Write Barrier)暂时中止操作,然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region (其他收集器:检查老年代对象是否引用了新生代对象)
如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中
当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set


G1回收过程

1、YoungGC

2、老年代并发标记过程

3、混合回收

(作为一种失败保护机制,Full GC还是存在的)


年轻代GC

当Eden空间耗尽时,G1会启动一次年轻代垃圾回收过程
年轻代垃圾回收只会回收Eden区和Survivor区

1、扫描根(根引用连同RSet记录的外部引用作为扫描存活对象的入口)

2、更新RSet( 此阶段完成后,RSet可以准确的反映老年代对所在的内存分段中对象的引用)

3、处理RSet(t:识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象)

4、复制对象(对象树被遍历,Eden区内存中存活的对象复制到Survivor区中空的内存分段,Survivor区内存段中存活的对象如果年龄未达阈值,年龄+1,达到阈值则被复制到Old区中空的内存分段。如果Survivor空间不够,Eden的部分数据会直接晋升到Old区)

5、处理引用


并发标记过程

1、初始标记阶段:标记从根节点直接可达的对象(STW+一次YoungGC)

2、根区域扫描:扫描Survivor区直接可达的老年代区域对象,并标记被引用的对象。这一过程必须在young GC之前完成(YoungGC时,会动Survivor区,所以这一过程必须在YoungGC之前完成)

3、并发标记:在整个堆中进行并发标记(和应用程序并发执行),此过程可能被YoungGC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)

4、再次标记:由于应用程序持续进行,需要修正上一次的标记结果(STW)

5、独占清理:计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域(STW,不会实际上去做垃圾的收集)

6、并发清理阶段:识别并清理完全空闲的区域


混合回收

当越来越多对象晋升到老年代时,为避免堆内存耗尽,虚拟机会触发一个混合的垃圾收集器

混合回收除了回收整个Young region外,还会回收一部分的Old Region(可以选择哪些Old region被回收,从而对整个垃圾回收的耗时进行控制)


G1优化建议

1、年轻代发送GC频率高,避免使用-Xmn-XX:NewRatio

2、暂停时间目标不要太过严苛(影响吞吐量)


GC日志分析

-XX:+PrintGC:输出GC日志

-XX:+PrintGCDetails:输出GC详细日志

-XX:+PrintGCTimeStamps:输出GC时间戳(以基准时间形式)

-XX:+PrintGCDateStamps:输出GC时间戳(以日期的形式)

-XX:+PrintHeapAtGC:在GC前后打印堆信息

-Xloggc:../logs/gc.log:日志文件的输出路径