G1收集器
G1概述
在收集器内部使用标记复制算法
来回收region区,但从整理来讲,有点像标记整理算法(将一个region区中存活的对象移动到另一块空region区
),JDK9默认GC
G1将堆划分成多个大小相同的独立region
,最多2048个
region大小 = 堆大小 / 2048
,可用参数-XX:G1HeapRegionSize调整(推荐默认值)
虽保留分代概念,但是逻辑分代,无物理隔阂,都是可以不连续的region
年轻代初始占比5%,会根据需要自动增加region,但年轻代最高占比不超过60%
,可以通过参数-XX:G1NewSizePercent调整
年轻代Eden和Survivor默认比例8:1:1
region区的角色是非固定的
,进入垃圾回收后,可能从年轻代转变成老年代
G1中的对象什么时候移动到老年代的机制和以前一样,唯一不同的是大对象的处理,大对象主要分配在Humongous区
,判断规则是: 如果对象超过region大小的一半,就会被放入Humongous区
,如果对象太大,会被分配到连续的几个region区
Humongous专门存放短暂的大对象
,不用直接进入老年代,主要是为了节约老年代空间,避免老年代空间不足进行GC
FullGC回收年轻代、老年代、Humongous中的垃圾对象
注意
: 一开始是没有老年代region区的,是边用边改变region区的角色,每做一次GC角色都可能不同,也就是逻辑分代
,没有物理隔阂
问题
: 如果对象大小超出一个region区范围呢
分开放几个连续的region区
G1运作步骤
详细链接: https://www.processon.com/view/link/61e839e4e401fd06afa1b4c9
G1运作步骤
STW对未标记的区域做清理,筛选回收阶段首先会对各个region的回收价值和成本进行排序,根据用户预期GC停顿时间来指定回收计划。在这个阶段不会把所有的垃圾都回收掉
,因为考虑到GC停顿时间,只会回收达到预期GC停顿时间内的区域
,剩余的区域会等下一次GC去回收
G1内部实现太复杂,暂时没有实现并发回收
,类似于Serial回收垃圾
STW修正并发标记期间因为用户线程继续运行而导致标记状态改变的对象
,这个阶段的停顿时间会比初始标记的时间稍长,远远比并发标记的时间段,主要是用到了三色标记
里面的原始快照
做重新标记
从gcroots的直接引用对象开始遍历整个对象图的过程
,耗时较长但是不需要暂停用户线程,可以与垃圾收集线程一起工作
。因为用户线程继续运行着,可能会导致已经标记的对象的状态发生了改变
,这也是后续重新标记的原因,并发标记占用整个运作过程的时间最长,大概占整个收集过程的80%
并发标记不进行STW的原因主要是为了用户体验
STW利用可达性分析算法找gcroots链上的直接引用对象
,这里不包含间接引用对象(它们的成员对象引用到的其他对象)
,速度很快
这里进行STW的原因主要是因为程序运行期间可能会不断有新的对象被引用,导致初始标记不完
初始标记(同CMS初始标记)
并发标记(同CMS并发标记)
最终标记(同CMS重新标记)
筛选回收
STW停顿时间不能乱指定,如果设置太小,可能会因为停顿很多region的垃圾未能回收
,时间长了会造成region堆积很多垃圾,最多引发一次很大规模的FullGC,这样反而会降低性能,最好设为100-200ms比较合理
问题
: G1可以控制STW时间,如果出现垃圾太多,回收时间超过设置的STW停顿时间如何处理
G1在回收之前会做一个预估,只回收大小在预期停顿时间内的垃圾对象,剩余部分等待下一次进行回收
,主要是为了用户体验,但是整个垃圾回收过程的时间会耽误更久,会牺牲一点吞吐量。
问题
: 回收一部分region区是怎么去选的,难道是随机
G1收集器在后台维护了一个优先列表
,每次根据允许的收集时间,优先回收价值较大的region区
比如: 一个region花200ms能回收10MB的垃圾,另一个花50ms能回收20MB,G1会选择后者region优先回收,这种机制保证G1在有限时间内尽可能达到最高的回收率
JDK7以上版本虚拟机的重要进化特征
并发/并行: G1充分利用CPU和多核环境下硬件优势,使用多核来缩短STW停顿时间。部分垃圾收集器比如Serial、Parallel需要停顿用户线程来进行GC,G1仍然可以通过并发让Java程序继续执行
分代收集: G1虽不需要配合其他收集器但也有逻辑分代
的概念
空间整合: 与CMS标记清除相比,G1采用局部复制算法
,从整体上看却是基于标记整理算法
来实现的收集器
可预测的停顿: 这是G1相对于CMS的另一大优势,降低停顿时间都是G1和CMS的共同点,但是G1还可以建立可预测的停顿时间模型
,让开发者明确指定停顿时间来完成垃圾回收,可以通过参数-XX:MaxGCPauseMillis来指定
问题
: 让用户指定可停顿的时间有什么好处呢
可以使得G1在不同场景根据需要平衡吞吐量和延迟
G1垃圾收集分类
YongGC(MinorGC)
回收年轻代的时间接近于预期停顿时间会触发
年轻代增加region,继续给对象存放,直到占比60%之后会触发
回收区域是年轻代
YoungGC并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数 -XX:MaxGCPauseMills 设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做YoungGC,直到下一次Eden区放满,G1计算回收时间接近参数 -XX:MaxGCPauseMills 设定的值,那么就会触发YoungGC
简单概括
MixedGC
老年代堆占有率达到整堆阈值(默认45%)
回收所有年轻代对象,部分老年代对象,大对象区域对象
也就是混合GC,非FullGC(个人理解就是类似于YoungGC和FullGC一起工作的)。老年代的堆占有率达到参数-XX:InitiatingHeapOccupancyPercent设定的值则触发,回收所有的年轻代和部分老年代(根据期望的GC停顿时间确定Old区垃圾收集的优先顺序)以及大对象区,正常情况G1的垃圾收集是先做MixedGC,主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次FullGC
简单概括
FullGC
在MixedGC之后没有足够空闲region用来做复制算法会触发
老年代空间分配担保机制会触发
停止系统线程,采用单线程进行标记d、清理以及整理,好空闲出一批region供下一次MixedGC使用,雷同Serial Old
停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的(Shenandoah优化成多线程收集了)
简单概括
问题
: G1的Eden区放满之后不一定触发MinorGC,那什么时候触发呢
会先判断Eden区的MinorGC回收时间是否接近与预期停顿时间,如果才会进行MinorGC
年轻代增加region,继续给对象存放,直到占比60%之后会触发
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%,此时就会立即停止混合回收,意味着本次混合回收就结束了
优化建议
最大停顿时间调整
导致系统运行很久,年轻代可能都占用了堆内存的60%了,此时才触发年轻代GC,那么存活下来的对象可能就会很多
,此时就会导致Survivor区域放不下那么多的对象
,就会进入老年代中
年轻代GC过后,存活下来的对象过多,导致进入Survivor区域后触发了动态年龄判定机制
,达到了Survivor区域的50%
,也会快速导致一些对象进入老年代中
最大停顿时间参数(-XX:MaxGCPauseMills)设置太大,会出现以下两种情况
这里核心还是在于调节最大停顿时间
-XX:MaxGCPauseMills 这个参数的值,在保证他的年轻代GC别太频繁的同时,还得考虑每次GC过后的存活对象有多少,避免存活对象太多快速进入老年代
,频繁触发MixedGC
什么场景适合G1
50%以上的堆被存活对象占用
对象分配和晋升的速度变化非常大
垃圾回收时间特别长,超过1秒
8GB以上的堆内存
(建议值)
停顿时间是500ms以内
每秒几十万并发的系统如何优化JVM
以kafka为例
不一定,得看情况,如果年轻代比老年代大很多,可达性分析都需要耗费很长时间
很显然,不可能,因为内存太大,处理还是要花不少时间的,假设三四十G内存回收可能最快也要几秒钟,按Kafka这个并发量放满三四十G的Eden区可能也就一两分钟吧,那么意味着整个系统每运行一两分钟就会因为YoungGC卡顿几秒钟没法处理新消息,显然是不行的。那么对于这种情况如何优化了,我们可以使用G1收集器,设置 -XX:MaxGCPauseMills 为50ms,假设50ms能够回收三到四个G内存,然后50ms的卡顿其实完全能够接受,用户几乎无感知,那么整个系统就可以在卡顿几乎无感知的情况下一边处理业务一边收集垃圾
Kafka类似的支撑高并发消息系统大家肯定不陌生,对于Kafka来说,每秒处理几万甚至几十万消息时很正常的,一般来说部署Kafka需要用大内存机器(比如64G),也就是说可以给年轻代分配个三四十G的内存用来支撑高并发处理,这里就涉及到一个问题了,我们以前常说的对于Eden区的YoungGC是很快的,这种情况下它的执行还会很快吗
G1天生就适合这种大内存机器的JVM运行
,可以比较完美的解决大内存垃圾回收时间过长的问题
问题
: 年轻代的GC一定比老年代GC快吗