1.垃圾回收器

1.GC分类与性能指标

垃圾收集器没有在规范中进行过多的规定 , 可以由不同的厂商 , 不同的版本JVM来实现
由于JDK的版本处于高速迭代过程中 , 因此Java发展至今已经衍生了众多的GC版本.
从不同角度分析垃圾收集器 , 可以将GC分为不同的类型

Java不同版本新特性

  • 语法层面: Lambda表达式 , switch , 自动拆箱装箱 , enum
  • API层面: Stream Api, 新的日期和时间 , Optional , String , 集合框架
  • 底层优化: JVM优化 . GC的变化 , 元空间 , 静态域, 字符串常量池位置变化

1.垃圾收集器分类

1.按照线程分

按照线程数分(垃圾回收线程数) , 可以分为串行垃圾回收器和并行垃圾回收器.
image.png
串行回收指的是在同一段时间内只允许有一个CPU用于执行垃圾回收操作, 此时工作线程被暂停 , 直至垃圾收集工作结束

  • 在诸如单CPU处理器或者比较小的应用内存等硬件平台不是特别优越的场合 , 串行回收器的表现可以超过并行回收器和并发回收器 , 所以 , 串行回收默认被应用在客户端的Client模式下的JVM中.
  • 在并发能力比较强的CPU上 , 并行回收器产生的停顿时间要短于串行回收器

和串行回收相反 , 并行收集可以运用多个CPU同时执行垃圾回收 , 因此提升了应用的吞吐量 , 不过并行回收仍然与串行回收一样 , 采用独占式 , 使用”stop-the-world”机制.

2.按工作模式分

按照工作模式分, 可以分为并发式垃圾回收器和独占式垃圾回收器

  • 并发式垃圾回收器和应用程序交替工作 , 以尽可能减少应用程序的停顿时间.
  • 独占式垃圾回收器(Stop The World)一旦运行 , 就停止应用程序中的所有用户线程, 知道垃圾回收过程完全结束

image.png

3.按碎片处理方式分

按照碎片处理方式分, 可以分为压缩垃圾回收器和非压缩垃圾回收器.

  • 压缩式垃圾回收器会在回收完成后 , 对存活对象进行压缩整理 , 消除回收后的碎片
  • 非压缩式的垃圾回收器不进行这步操作.

按照工作的内存区间分 , 又可分为年轻代垃圾回收器和老年代垃圾回收器.

2.评估GC的性能指标

  • 吞吐量: 运行用户代码的时间占总运行时间的比例(总运行时间=程序的运行时间 + 内存回收的时间)
  • 垃圾收集开销: 吞吐量的补数 , 垃圾收集所用时间与总运行时间的比例.
  • 暂停时间: 执行垃圾收集时 , 程序的工作线程被暂停的时间.
  • 收集频率: 相对于应用程序的执行 , 收集操作发生的频率.
  • 内存占用: Java堆区所占的内存大小
  • 快速: 一个对象从诞生到被回收锁经历的时间

吞吐量 , 暂停时间 , 内存占用 这三者共同构成一个”不可能三角”, 三者总体的表现会随着技术的进步而越来越好 , 一款优秀的收集器通常最多同时满足其中的两项, 这三项里, 暂停事件尤为重要 , 随着硬件发展 , 内存占用多越来越能容忍 , 硬件性能的提升也有助于降低收集器运行时对应用程序的影响, 即提高了吞吐量, 而内存的扩大 , 对延迟反而带来了负面效果 , 简单来说 , 主要两点

  • 吞吐量
  • 暂停时间

    3.性能指标: 吞吐量

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

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

这种情况下, 应用程序能容忍较高的暂停时间 , 因此 , 吞吐量的应用程序有更长的时间基准,快速响应是不必考虑的吞吐量优先, 意味着在单位时间内 , STW的时间最短0.2+0.2=0.4
image.png

4.性能指标: 暂停时间

暂停时间是指在一个时间段内应用程序线程暂停, 让GC线程执行的状态
例如: GC期间200ms的暂停时间就意味着这200ms期间没有应用程序是活动的 , 暂停时间优先, 暂停着尽可能单次STW的时间短: 0.2+0.2+0.2+0.2 = 0.8
image.png

5.吞吐量VS暂停时间

高吞吐量较好是因为这会让应用程序的最终用户感觉只有应用程序线程在做”生成性”直觉上 , 吞吐量越高程序运行越快.
低暂停时间(低延迟)较好因为从最终用户的的角度 , 来看不管是GC还是有其他原因导致的一个应用被挂起始终是不好的, 这取决于应用程序的类型, 有时候甚至短暂的200ms暂停都可能打断中断用户体验, 因此 , 具有较大暂停时间是非常重要的 , 特别是对于交互一个应用程序
不幸的是”高吞吐”和”低暂停”是一对相互的竞争目标
因为如果选择以高吞吐量优先, 那么必然需要降低内存回收的执行频率 , 但是这样就会导致GC需要更长的时间来执行内存回收了.
相反的, 如果选择以低延迟优先为原则 , 那么为了降低每次执行内存回收时的暂停时间 , 也只能频繁地执行内存回收, 但这又引起了年轻代内存的缩减和导致程序吞吐量的下降.
在设计(或使用)GC算法时, 我们必须确定我们的目标: 一个GC算法只可能针对两个目标之一(即只专注于较大吞吐量或者最小暂停时间)
现在标准: 在最大吞吐量优先的情况下 , 降低停顿时间

2.不同的垃圾回收器概述

1..垃圾回收发展史

有了虚拟机, 就一定要收集垃圾机制, 这就是Garbage Collection, 对应的我们的产品我们称为Garbage Collector

  • 1999年随JDK1.3.1吗一起来的是串行方式的SerialGC , 它是第一款GC,ParNew垃圾收集器时Serinal收集器的多线程版本
  • 2002年22月26号 , Parallel GC和Concurrent Mark Sweep GC跟随JDK1.4.2一起发布
  • Parallel GC在JDK1.6之后成为HotSpot默认的GC
  • 2012年, 在JDK1.7版本中, G1可用
  • 2017年,JDK9中G1变成默认的垃圾收集器, 以替代CMS.
  • 2018年3月,JDK10中G1变成默认的垃圾收集器 , 以替代CMS.
  • 2018年9月,JDK11发布。引入Epsilon 垃圾回收器,又被称为 “No-Op(无操作)“ 回收器。同时,引入ZGC:可伸缩的低延迟垃圾回收器(Experimental)
  • 2019年3月,JDK12发布。增强G1,自动返回未用堆内存给操作系统。同时,引入Shenandoah GC:低停顿时间的GC(Experimental)。·2019年9月,JDK13发布。增强zGC,自动返回未用堆内存给操作系统。
  • 2020年3月,JDK14发布。删除cMs垃圾回收器。扩展zGC在macos和Windows上的应用

    2. 7种经典的垃圾收集器

  • 串行回收器: Serial, Serial Old

  • 并行回收器: ParNew, Parallel Scavenge , Parallel Old
  • 并发回收器: CMS , G1

image.png

3. 7款经典收集器与垃圾分代之间的关系

image.png
新生代收集器: Serial. Parallel Scavenge , ParNew
老年代收集器: Serial Old, Parallel Old, CMS
整堆收集器: G1

4.垃圾收集器的组合关系

image.png

  • 两个收集器之间有连线, 表明它们可以搭配使用 , Serial/Serial Old . Serial/CMS , ParNew/Serial Old ParNew/CMS . Parallel Scavenge/Serial Old . Parallel Scavenge/Parallel Old
  • 其中Serial Old作为CMS出现”Concurrent Mode Failure”失败之后的预案/
  • (红色虚线, )由于维护和兼容性测试的版本 , 在JDK8时将Serial + CMS , ParNew+Serial Old这两个组合声明为废弃
  • 并在JDK9中完全取消这些组合支持
  • (青色虚线): JDK14. 弃用Parallel Scavenge和Serial Old GC组合,
  • (青色虚线): JDK14. 删除CMS垃圾回收器

    5.如何查看默认垃圾收集器

    -XX:PrintCommandLineFlags: 查看命令行相关参数(包括使用的垃圾收集器) ```shell -XX:InitialHeapSize=536870912 -XX:MaxHeapSize=8589934592 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
  1. 使用命令行指令 , jinfo -flag 相关垃圾回收器参数 进程ID
  2. ```shell
  3. jinfo -flag PrintCommandLineFlags [pId]

image.png

3.Serial回收器: 串行回收

Serial收集器是最基本 , 历史最悠久的垃圾收集器了, JDK1.3之前回收新生代唯一的选择.
Serial收集器作为HotSpot中client模式下的默认新生代垃圾收集器.
Serial收集器采用复制算法, 串行回收和”stop-the-World”机制的方式执行内存回收. 除了年轻代之外, Serial收集器还提供用于执行老年代收集的Serial Old收集器 , Serial Old收集器同样也采用了串行回收和”stop the World”机制, 只不过内存回收算法使用的是标记-压缩算法.

  • Serial Old是运行在Client模式下默认的老年代的垃圾回收器
  • Serial Old在Server模式下主要有两个用途
    • 与新生代的Parallel scavenge 配合使用
    • 作为老年代CMS收集器的后备垃圾收集方案.

image.png
这个收集器是一个单线程的收集器 ,但它的”单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作, 更重要的是它进行垃圾收集时, 必须暂停其他所有的工作线程,直到它收集结束(Stop The World)
优势: 简单而高效(与其他收集器的单线程比), 对于限定单个CPU的环境来说 , Serial收集器由于没有线程交互的开销 , 专心做垃圾收集自然可以获得最高的单线程收集效率.
运行在Client模式下的虚拟机是个不错的选择
在Hotspot虚拟机中 , 使用-XX:+UseSerialGC参数可以指定年轻代和老年代都使用串行收集器.
等价于新生代用Seriaal GC,且老年代用Serial Old GC

1.总结

对于交互较强的应用而言, 这种垃圾收集器是不能接受的 , 一般在Java Web应用程序中是不会采用串行收集器.

4.ParNew回收器: 并行回收

如果说serialGC是年轻代中的单线程垃圾收集器 , 那么ParNew收集器则是Serial收集器的多线程版本.

  • Par是Parallel的缩写, New: 只能处理的是新生代

ParNew 收集器除了采用并行回收的方式执行内存回收外 , 两款垃圾收集器之间几乎没有任何区别 .ParNew收集器在年轻代中同样也是采用复制算法 , “stop the world”机制
ParNew是很多JVM运行在Server模式下新生代的默认垃圾收集器.
image.png

  • 对于新生代, 回收次数频繁 , 使用并行方式高效
  • 对于老年代,回收次数少 , 使用串行方式节省资源.(CPU并行需要切换线程 , 串行可以省去切换线程的资源)
  • 由于ParNew收集器是基于并行回收的 , 那么是否可以断定ParNew收集器的回收效率在任何场景下都会比Serial收集器效率更高效?
    • ParNew 收集器运行在多CPU的环境下 , 由于可以充分利用CPU, 多核心等物理硬件资源优势 , 可以更快速完成垃圾收集 , 提升程序的吞吐量
    • 但是在单个CPU环境下,ParNew收集器比不上Serial收集器更高效 , 虽然Serial收集器时基于串行回收的, 但是由于CPU不需要频繁地做任务切换 , 因此可以有效避免多线程交互过程中产生的一些额外开销
  • 因为除了Serial外 , 目前只有ParNew GC能与CMS收集器配合工作

在开发中”-XX:+UseParNewGC”手动指定使用ParNew收集器执行内存回收任务 , 它表示年轻代使用并行收集器, 不影响老年代.
-XX: PararllelGCThreads限制线程数量, 默认开启和CPU相同的线程数

5.Parallel回收器: 吞吐量优先

HotSpot的年轻代中除了拥有ParNew收集器是基于并行回收的以外, Parallel Scavenge收集器同样也采用了复制算法, 并行垃圾和”Stop The World”机制.
那么Parallel收集器的特性:

  • 和ParNew收集器不同 , ParallelScavenge收集器的目标则是达到一个可控制的吞吐量(Throughput), 它也被称为吞吐量优先的垃圾收集器.
  • 自适应调节策略也是Parallel Scavenge 与ParNew一个重要区别.

高吞吐量则可以高效利用CPU时间, 尽快完成程序的运算任务 , 主要适合在后台运算而不需要太多交互的任务 , 因此 , 常见在服务器环境中使用 , 列如: 那些执行批量处理, 订单处理, 工资支付 , 科学计算的应用程序.
Parallel收集器在JDK1.6时提供了用于执行老年代垃圾收集的ParalleOld收集器 , 用来代替老年代的SerinalOld收集器
Paralle Old收集器采用标记-压缩算法 , 但同样也是基于并行回收和”stop-the-world”机制.
image.png
在程序吞吐量优先的应用场景中, Parallel 收集器和Parallel Old收集器的组合 , 在server模式下的内存回收性能很优秀

1.参数配置

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

  • 分配适用于新生代和老年代, 默认jdk8也是开启的
  • 上面两个参数 , 默认开启一个, 另外一个也会被开启(互相激活)

-XX:ParallelGCThreads设置年轻代并行收集器的线程数. 一般地 , 最好与CPU数量相等 , 以避免过多的线程数影响垃圾收集性能.
在默认情况下 , 当CPU数量小于8个,ParallelGCThreads的值等于CPU数量
当CPU数量大于8个, ParallelGCThreads的值等于3+(5*CPU Count/8)
-XX:MaxGCPauseMillis 设置垃圾收集器, 最大停顿时间(即STW的时间).单位是毫秒.默认最大

  1. -XX:MaxGCPauseMillis=18446744073709551615

为了尽可能地把停顿时间控制在MaxGCPauseMillis以内, 收集器在工作时会调整Java堆大小或者其他一些参数.对于用户来讲, 停顿时间越短体验越好 , 但是在服务端 , 我们注重高并发 , 整体的吞吐量 , 所以服务端适合Parallel,进行控制, 该参数使用需要谨慎.
-XX:GCTimeRatio垃圾收集时间占总时间的比例(=1/(N+1)) . 用于衡量吞吐量的大小
取值范围(0,100).默认是99, 也就是垃圾回收时间不超过1.
与前一个-XX:MaxGCPauseMillis参数存在一定性的矛盾性 , 暂停时间越长 , Radio参数就容易超过设定的比例.
-XX:+UseAdaptiveSizePolicy 设置Parallel Scavenge收集器具有自适应调节策略
在这种模式下, 年轻代的大小 , Eden和Survivor的比例 , 晋升老年代的对象年龄等参数会被自动调整 , 已达到在堆大小, 吞吐量和停顿时间之间的平衡点.
在手动调优比较困难的场合 , 可以直接使用这种自适应的方式 , 仅指定虚拟机的最大堆 , 目标的吞吐量(GCTimeRatio) 和停顿时间(MaxGCPauseMillis), 让虚拟机自己完成调优工作.

6.CMS(Concurrent Mark Sweep)回收器: 低延迟

在JDK1.5时期 , HotSpot推出一款在强交互应用中几乎可认为有时代意义的垃圾收集器: CMS(Concurrent Mark Sweep) 收集器 , 这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器 , 第一次实现了让垃圾收集线程与用户线程同时工作.
CMS收集器的关注点是尽可能缩短收集时用户线程的停顿时间, 停顿时间越短(低延迟)就越适合与用户交互的程序 , 良好的响应速度提升用户体验.
目前很大一部分的Java应用集中在互联网或者B/S系统的服务端上 , 这类应用尤其重视服务的响应速度.希望系统停顿时间最短 , 以给用户带来较好的体验 , CMS收集器就非常符合这类应用的需求.
CMS的垃圾收集算法采用标记-清除算法 , 并且也会”Stop The World”
不幸的是 , CMS作为老年代的收集器 , 却无法与JDK1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作 , 所以在JDK1.5中使用CMS来收集老年代的时候 , 新生代只能选择ParNew或者是Serial收集器中的一个.
在G1出现之前 , CMS使用还是很非常广泛的 , 一直到今天 , 仍然有很多系统使用CMS GC.
image.png
CMS整个过程比之前的收集器要复杂 , 整个过程中分为4个主要阶段 , 即初始标记阶段 , 并发标记阶段 , 重新标记阶段和并发清除阶段.(涉及STW的阶段主要是: 初始标记重新标记)

  • 初始标记(Initial-Mark)阶段: 在这个阶段中 , 程序中所有的工作线程都将会因为”Stop The World”机制而出现短暂的暂停 , 这个阶段的主要任务仅仅只是标记出GCRoots能直接关联到的对象. 一旦标记完成之后就会恢复之前被暂停的所有应用线程, 由于直接关联对象比较小 , 所以这里的速度非常快.
  • 并发标记(Concurrent-Mark)阶段: 从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程 , 可以与垃圾收集线程一起并发运行.
  • 重新标记(Remark)阶段; 由于在并发标记阶段中, 程序的工作线程会和垃圾收集线程同时运行或者交叉运行, 因此为了修正并发标记期间 , 因为用户程序继续运作而导致标记产生变动的那一部分对象的标记记录, 这个阶段的停顿时间通常会比初始标记阶段稍长一些 , 但也有远比并发标记阶段的时间短.
  • 并发清除(Concurrent-Sweep)阶段: 此阶段清理删除掉标记阶段判断的已经死亡的对象 , 释放内存空间 , 由于不需要移动存活对象 , 所以这个阶段是可以与用户线程同时并发的.

尽管CMS收集器采用的是并发回收(非独占式), 但是在其初始化标记和再次标记这两个阶段中仍然需要执行”Stop The World”机制暂停程序中的工作线程 , 不过暂停时间并不会太长 , 因此可以说明目前所有的垃圾收集器都做不到完全不需要”Stop The World”, 只是尽可能地缩短暂停时间.
由于最耗费时间的并发标记与并发清理阶段都不需要暂停工作 , 所以整体的回收时低停顿的.
另外,由于在垃圾收集阶段用户线程没有中断, 所以在CMS回收过程中 , 还应该确保应用程序用户线程有足够的内存可用, 因此, CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集 , 而是当堆内存使用率达到某一个阈值时, 便开始进行回收, 以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行. 要是CMS运行期间预留的内存无法满足程序需要 , 就会出现一次”Concurrent Mode Failure”失败,这时虚拟机将启动后备预案: 临时启动Serial Old收集器来重新进行老年代的垃圾收集, 这样就停顿时间就很长了.
CMS收集器的垃圾收集算法采用的是标记清除算法, 这意味着每次执行完内存回收后, 由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块 , 不可避免地将会产生一些内存碎片 , 那么CMS在为新对象分配内存空间时, 将无法使用指针碰撞(Bump the Pointer)技术 , 而只能够选择空闲列表(Free List)执行内存分配

1.CMS如果采用标记整理算法缺陷

当并发清除的时候 , 用Compact整理内存的话 , 原来的用户线程使用的内存还怎么用呢?
要保证用户线程能继续执行, 前提的它运行的资源不受影响嘛, Mark Compact 更加适合”stop the world”
这种场景下使用

2.优点

  • 并发收集
  • 低延迟

    3.缺点

  • 会产生内存碎片, 导致并发清除后, 用户线程可用的空间不足 , 在无法分配大对象的情况下, 不得不提前触发FullGC.

  • CMS收集器对CPU资源非常敏感, 在并发阶段 , 它虽然不会导致用户停顿 , 但是会因为占用一部分线程而导致应用程序变慢 , 总吞吐量会降低
  • CMS收集器无法处理浮动垃圾 , 可能出现”Concurrent Mode Failure”失败而导致另外一次Full GC的产生 , 在并发标记阶段由于程序的工作线程和垃圾收集线程同时运行或者交叉运行的, 那么在并发标记阶段如果产生新的垃圾对象 , CMS将无法对这些垃圾对象进行标记 , 最终会导致这些新产生的垃圾对象没有被即时回收,从而只能下一次执行GC时释放这些之前未被回收的内存空间.

    4.设置的参数

  • -XX:+UseConcMarkSweepGC手动指定使用CMS收集器执行内存回收任务. 开启该参数之后会将-XX:+UseParNewGC打开 , 即ParNew(Young区)+CMS(Old区)+Serial Old的组合.

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

JDK5及以前的版本默认值为68 , 即当老年代的空间使用率达到68% , 会执行一次CMS回收, JDK6及以上版本默认值为92%
如果内存增长缓慢 , 则可以设置一个稍大的值 , 大的阈值可以有效降低CMS的触发频率 , 减少老年代回收的次数可以较为明显地改善引用程序性能 , 反之 , 如果应用程序内存使用率增长很快 , 则应该降低这个阈值, 以避免频繁触发老年代串行收集器 , 因此通过该选项便可以有效降低FullGC的执行次数

  • -XX:UseCMSCompactAtFullCollection 用于指定在执行完Full

GC后内存空间进行压缩整理 , 以此避免内存碎片的产生 , 不过由于内存压缩整理过程无法并发执行, 所带来的问题就是停顿时间变得更长了.

  • -XX:CMSFullGCsBeforeCompaction 设置在执行多少次FullGC后对内存空间进行压缩整理
  • -XX:ParallelGCThreads 设置CMS的线程数量

CMS默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads是年轻代并行收集器的线程数 , 当CPU资源比较紧张时, 收到CMS收集线程的影响 , 应用程序的性能在垃圾回收节点可能会非常糟糕.

  • -XX:CMSScavengeBeforeRemark 它的作用是在重新标记之前对年轻代做一次minor GC,这样yong gen中剩余待标记的对象数量相比gc之前势必下降很多(只剩下存活的obj,大量死亡的obj被GC干掉了),剩余被视作“GC ROOTS”的对象数量骤减,如此Remark的工作量就少很多,重新标记的时间开销也会减少;当然这里Remark减少的时间和YGC的时间开销要做一个权衡,根据实践结果选择是否要开启,同样在Remark中我们也可以根据实际情况选择是否开启并行化-XX:+CMSParallelRemarkEnabled
  • -XX:+CMSParallelInitialMarkEnabled 初始化标记阶段,为了最大限度地减少STW的时间开销

    5.小结

    HotSpot进行垃圾回收的选择

  • SerialGC: 适合最小化的使用内存和并行开销

  • ParallelGC: 最大化应用程序的吞吐量
  • CMSGC: 最小化GC的中断或停顿时间

    6.JDK后续版本中CMS的变化

    JDK9新特性: CMS被标记为eprecate了如果JDK9及其以上版本的HotSpot虚拟机使用参数-XX:+UseConcMarkSweepGC的话, JVM不会报错 , 只是给出一个warning信息 , 但是不会exit, JVM会自动回退以默认的GC方式启动JVM

    7.G1(Garbage First)垃圾回收器:

    1. Garbage First(G1)解决问题

    因为G1是一个并行回收器 , 他把堆内存分割为很多不相关的区域(Region)(物理上不连续的 , ) 使用不同的Region来表示Eden , 幸存者0区, 幸存者1区 , 老年代
    G1GC有计划地避免在整个Java堆中进行全区域的垃圾收集 , G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需要的经验值 , 在后台维护一个优先列表), 每次根据允许的收集时间, 优先回收价值大的Region.
    由于这种方式的侧重点在于回收垃圾最大量的区间(Region), 所以我们给G1一个名字, 垃圾优先(GarBage First)
    G1(Garbage First)是一款面向服务端应用的垃圾收集器, 主要针对于配备多核CPU以及大容量内存的机器, 以极高概率满足GC停顿时间的同时 , 还兼具高吞吐量的性能特征
    在JDK1.7版本正式启用 , 移除了Experimental的表示 , 在JDK9以后的默认垃圾回收器 , 取代了CMS回收器以及Parallel+Parallel Old组合
    CMS在JDK9中标记为废弃, 在jdk8中还不是默认的垃圾回收器 , 需要使用-XX:+UseG1GC

2.G1垃圾收集器的优点

与其他GC收集器相比 , G1使用全新的分区算法 , 其特点如下所示:
并行与并发

  • 并行性: G1在回收期间,可以有多个GC线程同时工作 , 有效利用多核计算能力 , 此时用户线程STW
  • 并发性: G1拥有与应用程序交替执行的能力 , 部分工作可以和应用程序同时执行 , 因此 , 一般来说 , 不会再整个回收阶段发生完全阻塞应用程序的情况.

分代收集

  • 从分代上看, G1依然属于分代型垃圾回收器 , 它会区分年轻代和老年代, 年轻代依然有Eden区和Survivor区, 但从堆的结构上来看, 它不要求整个Eden区 , 年轻代或者老年代都是连续的, 也不再坚持固定大小和固定的数量
  • 将堆空间分为若干个区域(Region) , 这些区域中包含了逻辑上的年轻代和老年代
  • 和之前的各类回收器不同了 , 它同时兼顾年轻代和老年代, 对比其他回收器 , 或者工作在年轻代, 或者工作在老年代;

G1所谓的分代 , 已经不是下面的这样的了
image.png
而是一个
image.png
空间整合

  • CMS”标记-清除”算法, 内存碎片,几次GC之后进行一次碎片整理
  • G1将内存划分为一个个region, 内存的回收时region作为基本单位, region之间是复制算法 , 但整体上实际可看做是标记-压缩(Mark-Compact)算法, 两种算法都可以避免内存碎片, 这种特性有利于程序长时间运行, 分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC, 尤其是当Java堆非常大的时候 , G1的优势更加明显.

可预测的停顿时间模型(即:软实时soft real-time) 这是G1相对于CMS的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

  • 由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。
  • G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
  • 相比于CMSGC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多。

    3.G1垃圾收集器的缺点

    相比较于CMS, G1还不具备全方位, 压倒性优势 , 比如在用户程序运行过程中 , G1无论是为了垃圾收集器产生的内存占用还是程序运行时的额外执行负载都要比CMS要高
    如果是小内存应用上CMS的表现要优于G1, 而G1在大内存方面应用上则发挥其优势 , 平衡点在6-8G之间

    4.G1参数设置

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

  • -XX:G1HeapRegionSize 设置每个Region的大小 , 值是2的幂 , 范围是1MB到32MB之间 , 目标是根据最小的堆大小划分出去约2048个区域 , 默认是堆内存的1/2000
  • -XX:MaxGCPauseMillis设置期望达到的最大GC停顿时间指标, 默认是200ms
  • -XX:+ParallelGCThreads设置STW工作线程数的值 , 最多设置为8
  • -XX:ConcGCThreads设置并发标记的线程数 , 将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4
  • -XX:InitiatingHeapOccupancyPercent 设置触发并发GC周期的Java堆占用率阈值 , 超过此值 , 就触发GC. 默认值是45

    5. G1收集器的常见操作步骤

    G1的设计原则就是简化JVM性能调优

  • 开启G1垃圾收集器

  • 设置堆的最大内存
  • 设置最大的停顿时间

G1中提供了三种垃圾回收模式: YoungGC, Mixed GC和Full GC, 在不同的条件下被触发.

6.G1收集器的适用场景

面向服务端应用 , 针对于具有大内存 , 多处理器的机器 . (在普通大小的堆里表现并不惊喜)
最主要是应用是需要地GC延迟 , 并具有大堆的应用程序提供解决方案;
如: 在堆大小约6GB或更大时, 可预测的暂停时间可以低于0.5秒;(G1通过每次清理一部分而不是全部的Region的增量式清理保证每次GC停顿时间不会过长). 用来替换掉JDK1.5中的CMS收集器 ; 在下面的情况时 , 使用G1可能比CMS好.

  • 超过50%的Java堆被活动数据占用
  • 对象分配频率或年代提升频率变化很大;
  • GC停顿时间过长(长于0.5s至1s)

HotSpot垃圾收集器里 , 除了G1以外 , 其他的垃圾收集器使用内置的JVM线程执行GC的多线程操作 , 而G1GC可以采用应用线程承担后台运行的GC工作, 即当JVM的GC线程处理速度慢时 , 系统会调用应用程序线程帮助加速垃圾回收过程.

7.分区Region: 化整为零

使用G1收集器时 , 他将整个Java堆划分为约2048个大小相同的独立region块 , 每个Region根据堆空间的实际大小而定 , 整体被控制在1MB到32MB之间 , 而且为2的N次幂 , 即1MB / 2MB / 3 MB / 4MB 8MB/ 16MB / 32MB , 通过.
-XX:G1HeapRegionSize设定 , 所有的Region大小相同, 且在JVM生命周期内部会被改变的. 且在JVM生命周期内不会被改变
虽然还保留有新生代和老年代的概念 , 但是新生代和老年代不再是物理隔离的了 , 它们都是一部分region(不需要连续的集合) . 通过Region的动态分配方式实现逻辑上的连续
image.png
一个Region有可能属于Eden, Survivor或者Old/Tenured内存区域, 但是一个Region只可能属于一个角色, 图中的E表示该Region是属于Eden内存区域的, S区表示属于Survivor内存区域 , O表示Old内存区域, 图中空白的表示未使用的内存空间.
G1垃圾收集器还增加了一种新的内存区域 , 叫做Humongous内存区域 , 如图中的H块 , 主要用于存储大对象, 如果超过1.5region, 就会放到H.
设置H的原因 对于堆中的对象, 默认会直接分配到老年代 , 但是如果它是一个短期存在的大对象就会对垃圾收集器造成负面影响 , 为了解决这个问题 , G1划分了一个Humongous区 , 它用来专门存放大对象, 如果一个H区存放不下一个大对象 , 那么G1会寻找连续的H区来存储 , 为了能够找到连续的H区 , 有时候不得不启动FullGC, G1的大多数行为都把H区作为老年代的一部分来看待.
每个Region都是通过指针碰撞来分配空间的

8.G1垃圾回收器的回收过程

G1(Garbage First)的垃圾回收过程主要包括如下三个环节:

  • 年轻代GC(Young GC)
  • 老年代并发标记过程(Concurrent Marking)
  • 混合回收(Mixed GC)

(如果需要 , 单线程, 独占式 , 高强度的FullGC还是继续存在的 , 它针对GC的评估失败提供了一种失败保护机制 , 即强力回收)
image.png
顺时针, Young GC—->Young GC+Concurrent Mark + Mixed GC顺序 , 进行垃圾回收
应用程序分配内存 , 当年年轻代的Eden用尽开始老年代回收过程: G1的年轻代收集阶段是一个并行的独占式收集器 , 在年轻代回收期 , G1GC暂停所有应用程序线程 , 启动多线程执行年轻代回收 , 然后从年轻代区间移动存活对象到Survivor区间或者是老年代区间 , 有可能两个区都涉及
(默认是Eden/Survivor进行15次拷贝之后进入老年代 , 其次是年轻代空间不足 , 就会将大对象直接存入老年代)
当堆内存使用达到一定值(默认45%)时 , 开始老年代并发标记过程
标记完成马上开始混合回收过程 , 对于一个混合回收器 , G1GC从老年区移动存活对象到空闲区间 , 这些空闲区间也就成为了老年代的一部分 , 和年轻代不同, 老年代的G1回收器就和其他GC不同 , G1的老年代回收器不需要整个老年代被回收了 , 一次只需要扫描.回收一小部分老年代的Region就可以了 , 同时 , 这个老年代并发标记过程中, 标记完成偶开始4~5次混合回收.

9.Remembered Set (记忆集)

一个对象被不同的区域引用问题.
一个Region不不可能是独立的. 一个Region中的对象可能被其他任意Region中对象引用, 判断对象是否存活时, 是否需要扫描整个Java堆才能保证准确性?
在其他垃圾收集器中也存在这样的问题(Garbage First Collection)回收新生代也会扫描老年代, 这样就会降低Minor GC的效率
解决方法:
无论是G1收集器还是其他收集器都是采用Remembered Set避免全局扫描:每一个Region都有一个对应的Remembered Set; 每次Reference类型数据写操作是, 都会产生一个Write Barrier暂时中断操作;
然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(其他收集器: 检查老年代对象是否引用了新生代对象) ; 如果不同, 通过cardType把相关引用信息记录到引用指向对象的所在的Region对应的Remembered Set中;当进行垃圾收集时 , 在GC根节点的枚举范围加入Remember Set; 就可以保证不进行全局扫描 ,也不会有遗漏.

10. G1回收过程-年轻代GC

JVM启动时 , G1先准备好Eden区, 程序在运行过程中不断创建对象Eden区 , 当Eden空间耗尽时, G1会启动一次年轻代垃圾回收过程.