十五.吊打面试官系列-JVM优化-JVM垃圾回收器详解

前言

继上篇文章《JVM垃圾回算法详解》,我们本篇文章来讲解一下JVM垃圾回收器。垃圾标记算法是标记哪些对象是垃圾,垃圾回收算法是回收的理论算法,垃圾回收器才是真正回收垃圾的组件。不同的垃圾回收器使用了不同的垃圾回收算法

一.垃圾回收器

在 JVM 中,具体实现有 Serial、ParNew、Parallel Scavenge、CMS、Serial Old(MSC)、Parallel Old、G1 等。在下图中,你可以看到 不同垃圾回收器 适合于 不同的内存区域,如果两个垃圾回收器之间 存在连线,那么表示两者可以 配合使用,如下图:
在这里插入图片描述

  • 新生代可以适用的垃圾回收器:Serial(串行收集器)、ParNew(并行年轻代收集器)、Parallel Scavenge(并行回收器)
  • 老年代可以适用的垃圾回收器:CMS(并发标记-清除收集器)、Serial Old(串行老年代收集器)、Parallel Old(并行老年代收集器)
  • G1(Garbage First 收集器)和 ZGC(Z Garbage Collector)回收器适用于新生代和老年代混合回收
  • 相互之间有连线的表示可以配合使用,常用组合:Serial+Serial Old, Parallel Scavenge+Parallel Old,ParNew+CMS,G1(不需要组合其他收集器)

1.Serial 单线程垃圾回收器

  • 新生代 (Serial): -XX:+UseSerialGC
  • 老年代 (Serial Old): -XX:+UseSerialOldGC

Serial是一款用于新生代的单线程收集器,采用复制算法进行垃圾收集。Serial进行垃圾收集时,不仅只用一条线程执行垃圾收集工作,它在收集的同时,所有的用户线程必须暂停(Stop The World)。它们的设计初衷是为了适应早期的硬件环境和应用场景。在那个时候,硬件配置相对较低,主要特点包括内存容量较小、CPU 单核、并发应用场景相对较少。基于这些限制条件,Serial 系列的垃圾收集器采用了简单高效、资源消耗最少、单线程收集的设计思路。

但是Serial收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。Serial收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率

如下是Serial收集器和Serial Old收集器结合进行垃圾收集的示意图,当用户线程都执行到安全点时,所有线程暂停执行,Serial收集器以单线程,采用复制算法进行垃圾收集工作,收集完之后,用户线程继续开始执行。
在这里插入图片描述

Serial Old收集器是Serial收集器的老年代版本,它同样是一个单线程收集器采用标记整理算法。它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。

总结:Client模式(桌面应用), 适用于单核服务器,可以避免CPU切换,可以用-XX:+UseSerialGC来选择Serial作为新生代收集器。使用 XX:+UseSerialOldGC来选择SerialOld作为老年代收集器。Serial 新生代回收器 采用单线程+复制算法,SerialOld 老年代采用单线程+标记整理算法。

2.Parallel 多线程收集器(控制吞吐量)

  • 新生代(Parallel Scavenge) : -XX:+UseParallelGC
  • 老年代(Parallel Old) : -XX:+UseParallelOldGC)

随着硬件资源的升级,包括内存空间的增大和 CPU 的多核化,传统的 Serial 垃圾收集器面临着性能瓶颈。由于它采用单线程执行垃圾回收操作,无法充分利用多核 CPU 的优势,导致在处理大内存空间时性能下降,垃圾回收时间变得更长。为了充分发挥多核 CPU 的优势,JVM 推出了 Parallel 收集器系列。Parallel 收集器的设计思想是利用多线程并行执行垃圾回收操作,以提高整个垃圾收集过程的并行度和性能。

除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等
等)和Serial收集器类似。默认的收集线程数跟cpu核数相同,当然也可以用参数(-XX:ParallelGCThreads)指定收集线程数,但是一般不推荐修改。

Parallel Scavenge 回收器关心的是程序运行的吞吐量。吞吐量指的是一段时间内,用户代码 运行时间占 总运行时间 的百分比。那么如果用户代码的允许时间越长,吞吐量越高。可以通过-XX:MaxGCPauseMillis来设置收集器尽可能在多长时间内完成内存回收,可以通过-XX:GCTimeRatio来精确控制吞吐量。

如下是Parallel收集器和Parallel Old收集器结合进行垃圾收集的示意图,在新生代,当用户线程都执行到安全点时,所有线程暂停执行,Parallel收集器以多线程,采用复制算法进行垃圾收集工作,收集完之后,用户线程继续开始执行;在老年代,当用户线程都执行到安全点时,所有线程暂停执行,Parallel Old收集器以多线程,采用标记整理算法进行垃圾收集工作。
在这里插入图片描述
适用场景:注重吞吐量,高效利用CPU,需要高效运算且不需要太多交互。可以使用-XX:+UseParallelGC来选择Parallel Scavenge作为新生代收集器,jdk7、jdk8默认使用Parallel Scavenge作为新生代收集器。

3.ParNew(-XX:+UseParNewGC):新生代多线程垃圾回收器

ParNew 和 Parallel Scavenge 垃圾收集器在实现上确实有一些相似之处,都属于并行垃圾收集器。但 ParNew 垃圾收集器之所以出名,一个重要原因是它是唯一能与 CMS(Concurrent Mark-Sweep)收集器配合使用的新生代收集器,特别适用于那些对停顿时间要求较高的应用场景。

如下是ParNew收集器和Serial Old收集器结合进行垃圾收集的示意图,当用户线程都执行到安全点时,所有线程暂停执行,ParNew收集器以多线程,采用复制算法进行垃圾收集工作,收集完之后,用户线程继续开始执行。
在这里插入图片描述

适用场景:多核服务器;多与CMS收集器搭配使用。当使用-XX:+UseConcMarkSweepGC来选择CMS作为老年代收集器时,新生代收集器默认就是ParNew,也可以用-XX:+UseParNewGC来指定使用ParNew作为新生代收集器。

总结:ParNew 新生代回收器 采用的是 复制算法,多线程回收,多核CPU下,性能高于Serial

4. CMS(-XX:+UseConcMarkSweepGC)- 老年代垃圾回收器(最少停顿时间)

CMS(Concurrent Mark Sweep) 回收器是在 最短回收停顿时间 为前提的回收器,属于 多线程回收器,采用 标记-清除算法。它非常符合在注重用户体验的应用上使
用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作

在这里插入图片描述
相比之前的收集器,CMS收集器的运作过程比较复杂,分为四步:

  1. 初始标记:暂停所有的其他线程(STW),并记录下gc roots直接能引用的对象,速度很快。
  2. 并发标记:并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变
  3. 重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录(主要是处理漏标问题),会暂停用户线程(STW),这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法做重新标记。
  4. 并发清除:开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理
  5. 并发重置:重置本次GC过程中的标记数据

总结:CMS的有点在于尽可能减少用户线程的停顿时间,整个过程耗时最长的并发标记和并发清除都是和用户线程一起工作,所以从总体上来说,CMS收集器垃圾收集可以看做是和用户线程并发执行的。CMS收集器也存在一些缺点:

  1. 对CPU资源敏感:会和服务抢资源,默认分配的垃圾收集线程数为(CPU数+3)/4,随着CPU数量下降,占用CPU资源越多,吞吐量越小
  2. 无法清除浮动垃圾 : 由于CMS收集器清除已标记的垃圾(处于最后一个阶段)时,用户线程还在运行,因此会有新的垃圾产生,但是这部分垃圾未被标记,在下一次GC才能清除,因此被成为浮动垃圾。由于内存回收和用户线程是同时进行的,内存在被回收的同时,也在被分配。当老生代中的内存使用超过一定的比例时,系统将会进行垃圾回收;当剩余内存不能满足程序运行要求时,系统将会出现Concurrent Mode Failure,临时采用Serial Old算法进行清除,此时的性能将会降低
  3. 内存碎片化:它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生,当然通过参数-XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理
三色标记

在并发标记的过程中,因为标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。漏标的问题主要引入了三色标记算法来解决
三色标记算法是把Gc roots可达性分析遍历对象过程中遇到的对象, 按照“是否访问过”这个条件标记成以下三种颜色

  • 黑色:表示当前对象及对象的所有引用对象也已经被扫描过,处于存活状态的对象将会标记为黑色,黑色对象不会被再次扫描,也不会被回收。
  • 灰色:表示当前对象已经被扫描过,但是该对象存在部分应用对象未被扫描
  • 白色:表示对象尚未被垃圾收集器访问过。 显然在可达性分析刚刚开始的阶段, 所有的对象都是白色的, 若在分析结束的阶段,仍然是白色的对象, 即代表不可达,也就是垃圾。
浮动垃圾 - 多标

如果某GC Root对象及其应用对象都已经被扫描过标记为黑色,但是由于方法结束栈帧销毁导致GC Root对象释放,但是其引用的对象又被标记为黑色不可被回收,那么这样的对象称之为:浮动垃圾,这部分垃圾不会被回收需要等到下一次重新被扫描清除。
在这里插入图片描述

漏标情况

在并发标记过程中,用户线程也在一起工作,当某对象©失去引用,GC无法扫描到它将会被识别为垃圾(白色),等并发标记结束该对象©重新被其他对象引用(A),按道理说C现在不应该是垃圾了,但是在并发标记中已经被识别为垃圾,那么如果GC把C清理掉就会出现就是不合理的情况,C就是漏标的情况。
在这里插入图片描述
漏标会导致被引用的对象被当成垃圾误删除,这是严重bug,必须解决,有两种解决方案: 增量更新(Incremental Update) 和原始快照(Snapshot At The Beginning,SATB)

增量更新就是当黑色对象插入新的指向白色对象的引用关系时, 就将这个新插入的引用记录下来, 等并发扫描结束之后, 再将这些记录过的引用关系中的黑色对象为根, 重新扫描一次。 这可以简化理解为, 黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。

原始快照就是当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束之后, 再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑色(目的就是让这种对象
在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾)
以上无论是对引用关系记录的插入还是删除, 虚拟机的记录操作都是通过写屏障实现的。

写屏障

所谓的写屏障,其实就是指在赋值操作前后,加入一些处理,对于写屏障实现SATB来说,当对象B的成员变量的引用发生变化时,比如引用消失(b.c = null),我们可以利用写屏障,将B原来成员变量的引用对象C记录下以便后续再次扫描。

对于:写屏障实现增量更新当对象A的成员变量的引用发生变化时,比如新增引用(a.c = c),我们可以利用写屏障,将A新的成员变量引用对象c记录下来以便后续再次扫描。 CMS采用写屏障+增量更新,G1采用的是写屏障+SATB

记忆集和卡表

在新生代做GCRoots可达性扫描过程中可能会碰到跨代引用的对象,也就是老年代中的对象引用了新生代的对象,这种如果又去对老年代再去扫描效率太低。为此,在新生代可以引入记录集(Remember Set)的数据结构(记录从非收集区到收集区的指针集合)用来记录垮代引用的情况,避免把整个老年代加入GCRoots扫描范围。

hotspot使用一种叫做“卡表”(Cardtable)的方式实现记忆集,也是目前最常用的一种方式。关于卡表与记忆集的关系, 可以类比为Java语言中HashMap与Map的关系。
卡表是使用一个字节数组实现:CARD_TABLE[ ](在年轻代堆中),每个元素对应着其标识的内存区域一块特定大小(512KB)的内存块,称为“卡页”(把老年代分为大小相等等区域)

一个卡页中可包含多个对象,只要有一个对象的字段存在跨代指针,其对应的卡表的元素标识就变成1,表示该元素变脏,否则为0 。GC时,只要筛选本收集区的卡表中变脏的元素加入GCRoots里。

CMS相关参数

CMS的相关核心参数

  1. -XX:+UseConcMarkSweepGC:启用cms
  2. -XX:ConcGCThreads:并发的GC线程数
  3. -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
  4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次
  5. -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
  6. -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
  7. -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,降低CMS GC标记阶段(也会对年轻代一起做标记,如果在
    minor gc就干掉了很多对垃圾对象,标记阶段就会减少一些标记时间)时的开销,一般CMS的GC耗时 80%都在标记阶段
  8. -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
  9. -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;
CMS和其他收集器对比

这里我们简单对比一下几款垃圾回收器Serial就不说了,单线程工作,适用于单核机器,在如今基本上是不会被使用的。Parallel 和 CMS 而言,前者虽然是多线程工作但是在垃圾回收的过程中会触发STW机制这会导致用户线程停顿,如果对于JVM内存较大的情况下,那么垃圾回收的过程会比较长,用户停顿的时间也会比较长,这会可能会导致整体性能不好。所以:Parallel更适合小内存的情况下使用,比如:4G一下的内存。

而CMS初始标记会STW,但是这个阶段只会标记GC Root引用的对象,速度非常快,最耗时的并发标记是和用户线程一起工作的,不会停顿用户线程,重新标记是处理漏标的对象也比较快,所以整体来说。CMS是用户线程最少停顿的,如果内存较大的情况下建议使用CMS。性能较高。

5.G1 垃圾回收器

G1内存划分

G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征

传统的垃圾收集器把堆内存划分成老年代和年轻代,每次回收的粒度都是整个分代,G1垃圾收集器把堆内存划分成了更小的Region,在逻辑上还是有老年代和年轻代的概念,年轻代和老年代都拥有多个Region,堆内存中一个区域(Region)的大小可以通过-XX:G1HeapRegionSize参数指定,在每次垃圾回收时,G1会选择一些存活对象较少的Region进行回收,每个Region在逻辑上不是固定的年轻代和老年代,有可能现在是年轻代,下次垃圾回收后再分配时就可能成为老年代。
在这里插入图片描述
JVM最多可以有2048个Region。一般Region大小等于堆大小除以2048,比如堆大小为4096M,则Region大小为2M,当然也可以用参数"-XX:G1HeapRegionSize"手动指定Region大小,但是推荐默认的计算方式。

默认年轻代对堆内存的占比是5%,可以通过“-XX:G1NewSizePercent”设置新生代初始占比,在系统运行中,JVM会不停的给年轻代增加更多的Region,但是最多新生代的占比不会超过60%,可以通过“-XX:G1MaxNewSizePercent”调整。年轻代中的Eden和Survivor对应的region也跟之前一样,默认8:1:1。

一个Region可能之前是年轻代,如果Region进行了垃圾回收,之后可能又会变成老年代,也就是说Region的区域功能可能会动态变化。

G1垃圾收集器对于对象什么时候会转移到老年代跟之前讲过的原则一样,唯一不同的是对大对象的处理,G1有专门分配大对象的Region叫Humongous区,而不是让大对象直接进入老年代的Region中。在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,而且一个大对象如果太大,可能会横跨多个Region来存放。

Humongous区专门存放短期巨型对象,不用直接进老年代,可以节约老年代的空间,避免因为老年代空间不够的GC开销。Full GC的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收。

G1回收流程

G1的垃圾回收流程和CMS非常类似,分为四个步骤

在这里插入图片描述

  1. 初始标记:同CMS,暂停所有的其他线程(STW),并记录下gc roots直接能引用的对象,速度很快。
  2. 并发标记:同CMS,并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变
  3. 最终标记:同CMS的重新标记,最终标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录(主要是处理漏标问题),会暂停用户线程(STW),这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的原始快照SATB算法做最终标记。
  4. 筛选回收:该步骤也需要STW ,筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间(可以用JVM参数 -XX:MaxGCPauseMillis指定)来制定回收计划。也就是说在用户规定的时间范围内优先回收价值最高的Region,尽量把GC导致的停顿时间控制在我们指定的范围内。回收算法主要用的是复制算法,将一个region中的存活对象复制到另一个region中,然后把当前region清理掉,这种不会像CMS那样回收完因为有很多内存碎片还需要整理一次,G1采用复制算法回收几乎不会有太多内存碎片。

回收价值计算

G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来),比如一个Region花200ms能回收10M垃圾,另外一个Region花50ms能回收20M垃圾,在回收时间有限情况下,G1当然会优先选择后面这个Region回收。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内可以尽可能高的收集效率

总结一下G1垃圾回收器的特点

  • 并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop The-World停顿时间。部分其他收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
  • 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。
  • 空间整合:与CMS的“标记–清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
  • 可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段(通过参数"-XX:MaxGCPauseMillis"指定)内完成垃圾收集。

综合来看:G1不仅可以控制GC的垃圾回收时间,也可以做到和用户线程并发执行,使得G1在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡。

G1垃圾收集分类

YoungGC
YoungGC并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数 -XX:MaxGCPauseMills 设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做YoungGC,直到下一次Eden区放满,G1计算回收时间接近参数 -XX:MaxGCPauseMills 设定的值,那么就会触发Young GC

MixedGC
不是FullGC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent) 45% 设定的值则触发,回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区,正常情况G1的垃圾收集是先做 MixedGC,主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC

Full GC
停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的。(Shenandoah优化成多线程收集了)

为什么G1用SATB?CMS用增量更新?

SATB相对增量更新效率会高(当然SATB可能造成更多的浮动垃圾),因为不需要在重新标记阶段再次深度扫描被删除引用对象,而CMS对增量引用的根对象会做深度扫描,G1因为很多对象都位于不同的region,CMS就一块老年代区域,重新深度扫描对象
的话G1的代价会比CMS高,所以G1选择SATB不深度扫描对象,只是简单标记,等到下一轮GC再深度扫描

G1收集器参数设置

-XX:+UseG1GC:使用G1收集器
-XX:ParallelGCThreads:指定GC工作的线程数量
-XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是2的N次幂),默认将整堆划分为2048个分区
-XX:MaxGCPauseMillis:目标暂停时间(默认200ms)
-XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%)
-XX:G1MaxNewSizePercent:新生代内存最大空间
-XX:TargetSurvivorRatio:Survivor区的填充容量(默认50%),Survivor区域里的一批对象(年龄1+年龄2+年龄n的多个年龄对象)总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代
-XX:MaxTenuringThreshold:最大年龄阈值(默认15)
-XX:InitiatingHeapOccupancyPercent:老年代占用空间达到整堆内存阈值(默认45%),则执行新生代和老年代的混合收集(MixedGC),比如我们之前说的堆默认有2048个region,如果有接近1000个region都是老年代的region,则可能就要触发MixedGC了
-XX:G1MixedGCLiveThresholdPercent(默认85%) region中的存活对象低于这个值时才会回收该region,如果超过这个值,存活对象过多,回收的的意义不大。
-XX:G1MixedGCCountTarget:在一次回收过程中指定做几次筛选回收(默认8次),在最后一个筛选回收阶段可以回收一会,然后暂停回收,恢复系统运行,一会再开始回收,这样可以让系统不至于单次停顿时间过长。
-XX:G1HeapWastePercent(默认5%): gc过程中空出来的region是否充足阈值,在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清理掉,这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立即停止混合回收,意味着本次混合回收就结束了。

G1使用场景建议

  • 50%以上的堆被存活对象占用
  • 对象分配和晋升的速度变化非常大
  • 垃圾回收时间特别长,超过1秒
  • 8GB以上的堆内存(建议值)
  • 停顿时间是500ms以内

假设参数 -XX:MaxGCPauseMills 设置的值很大,导致系统运行很久,年轻代可能都占用了堆内存的60%了,此时才触发年轻代gc。那么存活下来的对象可能就会很多,此时就会导致Survivor区域放不下那么多的对象,就会进入老年代中。或者是你年轻代gc过后,存活下来的对象过多,导致进入Survivor区域后触发了动态年龄判定规则,达到了Survivor区域的50%,也会快速导致一些对象进入老年代中。所以这里核心还是在于调节 -XX:MaxGCPauseMills 这个参数的值,在保证他的年轻代gc别太频繁的同时,还得考虑每次gc过后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发mixed gc.

6.ZGC收集器(-XX:+UseZGC)[了解]

ZGC,全称为Z Garbage Collector,是Oracle公司开发的一款面向大型、多核、高内存应用的低延迟、高吞吐量的垃圾回收器,ZGC致力于将最大停顿时间控制在10毫秒以内,从而满足那些对响应时间有着极高要求的应用场景。此外,ZGC还支持从8MB到16TB的堆内存大小,为大内存应用提供了强有力的支持。

核心特性
  1. 低延迟:ZGC通过一系列优化措施,将GC暂停时间降低到毫秒级别。它采用了基于读屏障的堆栈式替换算法和基于标记颜色的压缩算法,避免了传统GC中的根扫描和整理等阶段,从而大幅减少了GC暂停时间。同时,ZGC还采用了并发标记和并发重分配等技术,使得大部分GC工作都可以与应用程序线程并发执行,进一步降低了应用程序的停顿时间。
  2. 高吞吐:虽然ZGC的主要目标是低延迟,但它的吞吐性能也非常出色。通过多线程并行处理垃圾回收任务,以及使用更大的堆空间和更高效的内存分配器等技术,ZGC能够在保证低延迟的同时,实现高吞吐量的垃圾回收
  3. 大堆支持:ZGC支持的最大堆内存大小为16TB,这使得它可以轻松应对云计算、大数据等领域的大内存应用。同时,ZGC还采用了基于Region的内存布局,将堆内存划分为多个固定大小的Region,从而提高了内存管理的灵活性和效率。
  4. 并发性:ZGC是一款并发的垃圾回收器,它可以在运行应用程序的同时进行垃圾回收操作。这种并发性使得ZGC能够充分利用多核CPU的并行处理能力,提高垃圾回收的效率和吞吐量。
  5. 透明性:ZGC对应用程序是透明的,应用程序无需进行任何修改即可使用ZGC进行垃圾回收。这种透明性使得ZGC的集成和使用变得非常简单和方便。
ZGC内存布局

ZGC收集器是一款基于Region内存布局的, 暂时不设分代的, 使用了读屏障、 颜色指针等技术来实现可并发的标记-整理算法的, 以低延迟为首要目标的一款垃圾收集器。ZGC的Region可以具有如图3-19所示的大、 中、 小三类容量:

  • 小型Region(Small Region) : 容量固定为2MB, 用于放置小于256KB的小对象。
  • 中型Region(Medium Region) : 容量固定为32MB, 用于放置大于等于256KB但小于4MB的对象。
  • 大型Region(Large Region) : 容量不固定, 可以动态变化, 但必须为2MB的整数倍, 用于放置4MB或
    以上的大对象。 每个大型Region中只会存放一个大对象, 这也预示着虽然名字叫作“大型Region”, 但它的实际容量完全有可能小于中型Region, 最小容量可低至4MB。 大型Region在ZGC的实现中是不会被重分配(重分配是ZGC的一种处理动作,用于复制对象的收集器阶段, 稍后会介绍到)的, 因为复制一个大对象的代价非常高昂。

在这里插入图片描述

回收流程

ZGC采用了基于标记-复制的算法,并对该算法进行了重大改进。在标记、转移和重定位阶段,ZGC几乎都是并发的,从而实现了极短的停顿时间。同时,ZGC还采用了染色指针和内存多重映射等技术,提高了内存管理的效率和灵活性。

具体来说,ZGC的工作过程可以大致分为以下几个阶段:

  1. 并发标记:与G1一样,并发标记是遍历对象图做可达性分析的阶段,它的初始标记(Mark Start)和最终标记(Mark End)也会出现短暂的停顿,与G1不同的是, ZGC的标记是在指针上而不是在对象上进行的, 标记阶段会更新染色指针中的Marked 0、 Marked 1标志位。
  2. 并发预备重分配::这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。ZGC每次回收都会扫描所有的Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本
  3. 并发重分配:将重分配集中的存活对象复制到新的Region上,并为每个Region维护一个转发表,记录从旧对象到新对象的转向关系。
  4. 并发重映射:重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,但是ZGC中对象引用存在“自愈”功能,所以这个重映射操作并不是很迫切。ZGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。一旦所有指针都被修正之后, 原来记录新旧对象关系的转发表就可以释放掉了
ZGC触发时机

ZGC目前有4中机制触发GC:

  • 定时触发,默认为不使用,可通过ZCollectionInterval参数配置。
  • 预热触发,最多三次,在堆内存达到10%、20%、30%时触发,主要时统计GC时间,为其他GC机制使用。
  • 分配速率,基于正态分布统计,计算内存99.9%可能的最大分配速率,以及此速率下内存将要耗尽的时间点,
    在耗尽之前触发GC(耗尽时间 - 一次GC最大持续时间 - 一次GC检测周期时间)。
  • 主动触发,(默认开启,可通过ZProactive参数配置) 距上次GC堆内存增长10%,或超过5分钟时,对比距上次GC的间隔时间跟(49 * 一次GC的最大持续时间),超过则触发。
ZGC存在的问题

ZGC最大的问题是浮动垃圾。ZGC的停顿时间是在10ms以下,但是ZGC的执行时间还是远远大于这个时间的。假如ZGC全过程需要执行10分钟,在这个期间由于对象分配速率很高,将创建大量的新对象,这些对象很难进入当次GC,所以只能在下次GC的时候进行回收,这些只能等到下次GC才能回收的对象就是浮动垃圾

7.如何选择垃圾收集器

  1. 优先调整堆的大小让服务器自己来选择
  2. 如果内存小于100M,使用串行收集器
  3. 如果是单核,并且没有停顿时间的要求,串行或JVM自己选择
  4. 如果允许停顿时间超过1秒,选择并行或者JVM自己选
  5. 如果响应时间最重要,并且不能超过1秒,使用并发收集器
  6. 4G以下可以用parallel,4-8G可以用ParNew+CMS,8G以上可以用G1,几百G以上用ZGC

在这里插入图片描述

二.安全点与安全区域

在Java虚拟机(JVM)中,安全点和安全区域是两个与垃圾收集(GC)过程密切相关的概念。它们共同确保了垃圾收集过程的正确性和高效性。

1.安全点(Safepoint)

  • 定义:安全点是JVM在程序执行过程中选定的一些特定位置。当应用程序线程执行到这些位置时,它们会安全地暂停下来,以便JVM执行GC线程进行垃圾回收。
  • 选取位置:安全点的选取一般是基于循环结束、方法调用前后、异常跳转等指令位置。这些位置都是JVM在字节码指令中预先设定的。
  • 作用:通过安全点,JVM可以确保在垃圾收集过程中,所有的执行线程都处于一个稳定的状态,从而避免了对堆中对象的非法访问。

2.安全区域(Safe Region)

  • 定义:安全区域是指在一段代码片段中,引用关系不会发生改变的区域。只要线程处在这个区域中,外部如果要进行垃圾回收,它就是安全的。
  • 特性:在安全区域内,执行线程不能够直接操作堆中的对象,从而保证了垃圾收集器能够正确地执行。
  • 处理机制:当应用程序线程进入安全区域时,它会标识自己已经进入了安全区域。这样,GC线程在执行垃圾收集时,就可以忽略这些处于安全区域的线程。当线程要离开安全区域时,它会检查GC是否已经完成。如果完成了,则继续执行;否则,线程必须等待,直到收到可以离开安全区域的信号。

总结来说,JVM通过安全点和安全区域这两个机制,确保了垃圾收集过程的正确性和高效性。安全点使得JVM能够在特定的代码位置暂停应用程序的执行,而安全区域则保证了在垃圾收集期间,执行线程对堆中对象的访问是安全的。这两个机制的协同工作,为Java程序的稳定运行提供了有力的保障

文章结束喜欢请各个好评哦!!!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

墨家巨子@俏如来

你的鼓励是我最大的动力

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

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

打赏作者

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

抵扣说明:

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

余额充值