G1和ZGC垃圾回收器

G1垃圾回收器

G1回收器基本概念

统一收集新生代 和老年代,采用了更加优秀的算法和设计机制。

1、ParNew + CMS的组合让我们有哪些痛点?

Stop the World - STW,这个是大家最痛的一个点!

​ 无论是新生代垃圾回收,还是老年代垃圾回收,都会或多或少产生“Stop the World”现象,对系统的运行是有一定影响的。

所以其实之后对垃圾回收器的优化,都是朝着减少“Stop the World”的目标去做的。

在这个基础之上,G1垃圾回收器就应运而生了,他可以提供比“ParNew + CMS”组合更好的垃圾回收的性能。

2、G1垃圾回收器

​ G1垃圾回收器是可以同时回收新生代和老年代的对象的,不需要两个垃圾回收器配合起来运作,他一个人就可以搞定所有的垃圾回收。

他最大的一个特点,就是把Java堆内存拆分为多个大小相等的Region,如下图。

image-20191218102752760

然后G1也会有新生代和老年代的概念,但是只不过是逻辑上的概念

也就是说,新生代可能包含了某些Region,老年代可能包含了某些Reigon,需要注意的是,新生代跟老年代可能就不是连续的一块内存了,他们由多个region组成,分散在四处,如下图。

image-20191218102808577

而且G1最大的一个特点,就是可以让我们设置一个垃圾回收的预期停顿时间

也就是说比如我们可以指定:希望G1在垃圾回收的时候,可以保证,在1小时内由G1垃圾回收导致的“Stop the World”时间,也就是系统停顿的时间,不能超过1分钟。

​ 以前我们很多JVM优化的思路,最终是对内存合理分配,优化一些参数,就是为了尽可能减少Minor GC和Full GC,尽量减少GC带来的系统停顿,避免影响系统处理请求。

​ 但是现在我们直接可以给G1指定,在一个时间内,垃圾回收导致的系统停顿时间不能超过多久,G1全权给你负责,保证达到这个目标。

这样相当于我们就可以直接控制垃圾回收对系统性能的影响了。

3、G1是如何做到对垃圾回收导致的系统停顿可控的?

其实G1如果要做到这一点,他就必须要追踪每个Region里的回收价值,啥叫做回收价值呢?

他必须搞清楚每个Region里的对象有多少是垃圾,如果对这个Region进行垃圾回收,需要耗费多长时间,可以回收掉多少垃圾?

​ 大家看下图,G1通过追踪发现,1个Region中的垃圾对象有10MB,回收他们需要耗费1秒钟,另外一个Region中的垃圾对象有20MB,回收他们需要耗费200毫秒。

03_Region追踪 改.jpg

​ 然后在垃圾回收的时候,G1会发现在最近一个时间段内,比如1小时内,垃圾回收已经导致了几百毫秒的系统停顿了,现在又要执行一次垃圾回收,那么必须是回收上图中那个只需要200ms就能回收掉20MB垃圾的Region啊!

于是G1触发一次垃圾回收,虽然可能导致系统停顿了200ms,但是一下子回收了更多的垃圾,就是20MB的垃圾,如下图。

04_回收Region.jpg

所以简单来说,G1可以做到让你来设定垃圾回收对系统的影响,他自己通过把内存拆分为大量小Region,以及追踪每个Region中可以回收的对象大小和预估时间,最后在垃圾回收的时候,尽量把垃圾回收对系统造成的影响控制在你指定的时间范围内,同时在有限的时间内尽量回收尽可能多的垃圾对象。

这就是G1的核心设计思路

4、Region可能属于新生代也可能属于老年代

另外在G1中,每一个Region时可能属于新生代,但是也可能属于老年代的。

刚开始Region可能谁都不属于,然后接着就分配给了新生代,然后放了很多属于新生代的对象,接着就触发了垃圾回收这个Region,如下图。

05_回收新生代region改.jpg

然后下一次同一个Region可能又被分配了老年代了,用来放老年代的长生存周期的对象,如下图所示。

image-20191218103846669

​ 所以其实在G1对应的内存模型中,Region随时会属于新生代也会属于老年代,所以没有所谓新生代给多少内存,老年代给多少内存这 一说了

实际上新生代和老年代各自的内存区域是不停的变动的,由G1自动控制。

5、g1 回收算法 - 也是复制算法

为了保证碎片化的不存在,回收一个region的时候,会把存活对象,复制到其他region。然后清空回收的region

本文总结

​ 本文先初步的介绍一下G1垃圾回收器的设计思想,包括Region的划分,然后Region动态转移给新生代或者老年代,按需分配,某个region某个时刻,可能是属于新生代,但是下一个时刻,他可能就是属于老年代(例如一个region回收之后,未来可以分配给老年代也可能是新生代)。所以是动态的,但是新生代和老年代的内存比率还是存在的,下面会讲。

然后触发垃圾回收的时候,可以根据设定的预期系统停顿时间,来选择最少回收时间和最多回收对象的Region进行垃圾回收(选择性价比最高的region),保证GC对系统停顿的影响在可控范围内,同时还能尽可能回收最多的对象。

猜测,G1回收器,适合对STW特别敏感的业务上,因为不能让系统停摆太长时间。可能是实时通信之类的,也就是一些追求低延迟的响应的业务。

又或者是,高内存的机器,例如16G,32G等,如果不用G1回收器,那么回收的时间就太长了(堆大小很大,那么回收的垃圾就越多),stw的时间也就越长。用了G1就可以控制,只回收部分region即可,不用回收全部新生代或者老年代。

G1进阶理解1

2、如何设定G1对应的内存大小

我们知道G1对应的是一大堆的Region内存区域,每个Region的大小是一致的。

那么首先思考两个问题:到底有多少个Region呢?每个Region的大小是多大呢?

​ 其实这个默认情况下自动计算和设置的,我们可以给整个堆内存设置一个大小,比如说用“-Xms”和“-Xmx”来设置堆内存的大小。

​ 然后JVM启动的时候一旦发现你使用的是G1垃圾回收器,可以使用“-XX:+UseG1GC”来指定使用G1垃圾回收器,此时会自动用堆大小除以2048。

​ 因为JVM最多可以有2048个Region,然后Region的大小必须是2的倍数,比如说1MB、2MB、4MB之类的(因为假设不能整除2048,那么为了满足region大小必须是2的倍数,那么肯定存在,某些region的值是不一样的)。

​ 比如说堆大小是4G,那么就是4096MB,此时除以2048个Region,每个Region的大小就是2MB。大概就是这样子来决定Region的数量和大小的,大家一般保持默认的计算方式就可以

如果通过手动方式指定region大小则是“-XX:G1HeapRegionSize”但是需要注意,假设堆内存你分配的是8G,-XX:G1HeapRegionSize=2m,因为JVM最多有2048个region。那么也就是2*2048=4G,也就意味着,这里你只用了4G堆内存,还有4G就浪费了,所以这个时候需要设置-XX:G1HeapRegionSize=4m。如下图。

image-20191218105658603

​ 刚开始的时候,默认新生代对堆内存的占比是5%,也就是占据200MB左右的内存,对应大概是100个Region,这个是可以通过“-XX:G1NewSizePercent”来设置新生代初始占比的,其实维持这个默认值即可。

​ 因为在系统运行中,JVM其实会不停的给新生代增加更多的Region,但是最多新生代的占比不会超过60%,可以通过“-XX:G1MaxNewSizePercent”

也就是说,新生代的默认内存区间是 5% ~ 60%

而且一旦Region进行了垃圾回收,此时新生代的Region数量还会减少,这些其实都是动态的

大家看下图,刚开始就是一部分的Region是属于新生代的。

image-20191218105747097

3、新生代还有Eden和Survivor的概念吗?

G1中虽然把内存划分为了很多的 Region,但是其实还是有新生代、老年代的区分(但都是逻辑上的概念,他们可能分散在各个region,并不是连续的一块内存)。

而且新生代里还是有Eden和Survivor的划分的,所以大家会发现之前学习的很多技术原理在G1时期都是有用的。

​ 大家应该还记得之前说过的一个新生代的参数,“-XX:SurvivorRatio=8”,所以这里还是可以区分出来属于新生代的Region里哪些属于Eden,哪些属于Survivor。

​ 比如新生代之前说刚开始初始的时候,有100个Region,那么可能80个Region就是Eden,两个Survivor各自占10个Region,如下图。

image-20191218105828904

​ 所以大家要明白在这里其实还是有Eden和Survivor的概念的,他们会各自占据不同的Region。

只不过随着对象不停的在新生代里分配,属于新生代的Region会不断增加,Eden和Survivor对应的Region也会不断增加。

4、G1的新生代垃圾回收触发点和如何设置回收时间

​ 既然G1的新生代也有Eden和Survivor的区分,那么触发垃圾回收的机制都是类似的。

​ 随着不停的在新生代的Eden对应的Region中放对象,JVM就会不停的给新生代加入更多的Region,直到新生代占据堆大小的最大比例60%

​ 一旦新生代达到了设定的占据堆内存的最大大小60%,比如都有1200个Region了,里面的Eden可能占据了1000个Region,每个Survivor是100个Region,而且Eden区还占满了对象,此时如下图所示。

image-20191218105939677

​ 这个时候还是会触发新生代的GC,G1就会用之前说过的复制算法来进行垃圾回收进入一个“Stop the World”状态。(但是stw的的时间是可以控制的)

​ 然后把Eden对应的Region中的存活对象放入S1对应的Region中,接着回收掉Eden对应的Region中的垃圾对象,如下图。

image-20191218110042480

但是这个过程跟之前是有区别的,因为G1是可以设定目标GC停顿时间的,也就是G1执行GC的时候最多可以让系统停顿多长时间,那么也就意味着,GC完成之后,肯定存在某些region 的垃圾是不能回收的,因为需要在设定的时间内进行回收垃圾

​ 可以通过“-XX:MaxGCPauseMills”参数来设定,默认值是200ms。

​ 那么G1就会通过之前说的,对每个Region追踪回收他需要多少时间,可以回收多少对象来选择回收一部分的Region,保证GC停顿时间控制在指定范围内,尽可能多的回收掉一些对象。

问题,难道G1真的要等新生代内存使用到60%后,才进行新生代回收?很明显不是,我们后面会讲

5、对象什么时候进入老年代?

大家都知道,在G1的内存模型下,新生代和老年代各自都会占据一定的Region,老年代也会有自己的Region

按照默认新生代最多只能占据堆内存60%的Region来推算,老年代最多可以占据40%的Region,大概就是800个左右的Region。

那么对象什么时候从新生代进入老年代呢?

可以说跟之前几乎是一样的,还是这么几个条件:

(1)对象在新生代躲过了很多次的垃圾回收,达到了一定的年龄了,“-XX:MaxTenuringThreshold”参数可以设置这个年龄,他就会进入老年代

(2)动态年龄判定规则,如果一旦发现某次新生代GC过后,存活对象超过了Survivor的50%

​ 此时就会判断一下,比如年龄为1岁,2岁,3岁,4岁的对象的大小总和超过了Survivor的50%,此时4岁以上的对象全部会进入老年代,这就是动态年龄判定规则

大家看下图,所以经过一段时间的新生代使用和垃圾回收之后,总有一些对象会进入老年代中。

image-20191218110244416

6、大对象Region

​ 大家此时可能会疑惑了,唉?以前说是那种大对象也是可以直接进入老年代的,那么现在在G1的这套内存模型下呢?

实际上这里会有所改变,G1提供了专门的Region来存放大对象,而不是让大对象进入老年代的Region中。

​ 在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,比如按照上面算的,每个Region是2MB,只要一个大对象超过了1MB,就会被放入大对象专门的Region中。

而且一个大对象如果太大,可能会横跨多个Region来存放。如下图。

image-20191218110350081

肯定还有人会问,那堆内存里哪些Region用来存放大对象啊?

不是说60%的给新生代,40%的给老年代吗,那还有哪些Region给大对象?

很简单,之前说过了,在G1里,新生代和老年代的Region是不停的变化的

​ 比如新生代现在占据了1200个Region,但是一次垃圾回收之后,就让里面1000个Region都空了,此时那1000个Region就可以不属于新生代了,里面很多Region可以用来存放大对象。

那么还有人会问了,大对象既然不属于新生代和老年代,那么什么时候会触发垃圾回收呢?

​ 也很简单,其实新生代、老年代在回收的时候,会顺带带着大对象Region一起回收,所以这就是在G1内存模型下对大对象的分配和回收的策略。

其他回收器大对象的判断标准是通过一个参数来设定,就是“-XX:PretenureSizeThreshold”,可以把他的值设置为字节数,比如“1048576”字节,就是1MB。G1回收器判断是否是大对象的依据是,是否大于region的百分之五十。

G1进阶理解2

1、前文回顾

​ 现在大家已经搞清楚了G1的动态内存管理策略,他会根据情况动态的把Region分配给新生代、Eden、Survivor、老年代和大对象,但是新生代和老年代有一个各自的最大占比,然后在新生代的Eden满的时候,触发新生代垃圾回收。

新生代的垃圾回收还是采用了复制算法,只不过会考虑预设GC停顿时间,保证垃圾回收的停顿时间不能超过预设时间,因此会挑选一些Region来进行垃圾回收。

然后跟之前说的一样,如果一些对象在新生代熬过了一定次数的GC,或者是触发了动态年龄判定规则,或者是存活对象在Survivor放不下了,都会让对象进入老年代中。

而大对象则是进入单独的大对象Region,不再进入老年代。

所以实际上在G1中,还是会存在新生代的对象慢慢会因为各种情况进入老年代的。

2、什么时候触发新生代+老年代的混合垃圾回收?

G1有一个参数,是“-XX:InitiatingHeapOccupancyPercent”,他的默认值是45%

​ 意思就是说,如果老年代占据了堆内存的45%的Region不是说,新生代占据了百分之60么?老年代怎么会有百分之四十五?那是新生代最多占据%60,所以老年代是有可能到%45)的时候,此时就会尝试触发一个新生代+老年代一起回收的混合回收阶段。

​ 比如按照我们之前说的,堆内存有2048个Region,如果老年代占据了其中45%的Region,也就是接近1000个Region的时候,就会开始触发一个混合回收,如下图所示。

image-20191218113037224

3、G1垃圾回收的过程

你会发现G1的回收过程跟CMS回收器的步骤一样,也是四个步骤。

​ 首先会触发一个“初始标记”的操作,这个过程是需要进入“Stop the World”的,仅仅只是标记一下GC Roots直接能引用的对象,这个过程速度是很快的。

​ 如下图,先停止系统程序的运行,然后对各个线程栈内存中的局部变量代表的GC Roots,以及方法区中的类静态变量代表的GC Roots,进行扫描,标记出来他们直接引用的那些对象

image-20191218113917147

​ 接着会进入“并发标记”的阶段,这个阶段会允许系统程序的运行,同时进行GC Roots追踪,从GC Roots开始追踪所有的存活对象,如下图所示。

image-20191218113956478

这里对GC Roots追踪做更加详细的说明,比如下面的代码

​ 大家可以看到,Kafka这个类有一个静态变量是“replicaManager”,他就是一个GC Root对象,初始标记阶段,仅仅就是标记这个“replicaManager”作为GC Roots直接关联的对象,就是“ReplicaManager”对象,他肯定是要存活的。

​ 然后在并发标记阶段,就会进行GC Roots追踪,会从“replicaManager”这个GC Root对象直接关联的“ReplicaManager”对象开始往下追踪

​ 可以看到“ReplicasManager”对象里有一个实例变量“replicaFetcher”,此时追踪这个“replicaFetcher”变量可以看到他引用了“ReplicaFetcher”对象,那么此时这个“ReplicaFetcher”对象也要被标记为存活对象。

blob.png

这个并发标记阶段还是很耗时的,因为要追踪全部的存活对象。

但是这个阶段是可以跟系统程序并发运行的,所以对系统程序的影响不太大。

而且JVM会对并发标记阶段对对象做出的一些修改记录起来,比如说哪个对象被新建了,哪个对象失去了引用。

​ 接着是下一个阶段,最终标记阶段,这个阶段会进入“Stop the World”,系统程序是禁止运行的,但是会根据并发标记 阶段记录的那些对象修改,最终标记一下有哪些存活对象,有哪些是垃圾对象,如下图所示。

image-20191218114128560

​ 最后一个阶段,就是“混合回收“阶段,这个阶段会计算老年代中每个Region中的存活对象数量,存活对象的占比,还有执行垃圾回收的预期性能和效率。

接着会停止系统程序,然后全力以赴尽快进行垃圾回收,此时会选择部分Region进行回收,因为必须让垃圾回收的停顿时间控制在我们指定的范围内。

​ 比如说老年代此时有1000个Region都满了,但是因为根据预定目标,本次垃圾回收可能只能停顿200毫秒,那么通过之前的计算得知,可能回收其中800个Region刚好需要200ms,那么就只会回收800个Region,把GC导致的停顿时间控制在我们指定的范围内,如下图。

image-20191218114217544

而且大家需要在这里有一点认识,其实老年代对堆内存占比达到45%的时候,触发的是“混合回收”

也就是说,此时垃圾回收不仅仅是回收老年代,还会回收新生代,还会回收大对象。

那么,到底是回收这些区域的哪些Region呢?

那就要看情况了,因为我们设定了对GC停顿时间的目标,所以说他会从新生代、老年代、大对象里各自挑选一些Region,保证用指定的时间(比如200ms)回收尽可能多的垃圾,这就是所谓的混合回收,如下图。

image-20191218114324205

4、G1垃圾回收器的一些参数

​ 大家在上面都看到了,一般在老年代的Region占据了堆内存的Region的45%之后,会触发一个混合回收的过程,也就是Mixed GC,分为了好几个阶段

​ 在这里最后一个环节,其实就是执行混合回收,从新生代和老年代里都回收一些Region。

​ 但是最后一个阶段混合回收的时候,其实会停止所有程序运行,所以说G1是允许执行多次混合回收。

​ 比如先停止工作,执行一次混合回收回收掉 一些Region,接着恢复系统运行,然后再次停止系统运行,再执行一次混合回收回收掉一些Region。

​ 有一些参数可以控制这个,比如“-XX:G1MixedGCCountTarget”参数,就是在一次混合回收的过程中,最后一个阶段执行几次混合回收,默认值是8次

​ 意味着最后一个阶段,先停止系统运行,混合回收一些Region,再恢复系统运行,接着再次禁止系统运行,混合回收一些Region,反复8次。

​ 如下图,假设一次混合回收预期要回收掉一共有160个Region,那么此时第一次混合回收,会回收掉一些Region,比如就是 20个Region。

image-20191218114533978

接着恢复系统运行一会儿,然后再执行一次“混合回收”,如下图,再次回收掉20个Region。

image-20191218114541987

​ 如此反复执行8次混合回收阶段之后 ,不就把预订的160个Region都回收掉了?而且还把系统停顿时间控制在指定范围内。

​ 那么为什么要反复回收多次呢?

​ 因为你停止系统一会儿,回收掉一些Region,再让系统运行一会儿,然后再次停止系统一会儿,再次回收掉一些Region,这样可以尽可能让系统不要停顿时间过长,可以在多次回收的间隙,也运行一下。

还有一个参数,就是“-XX:G1HeapWastePercent”,默认值是5%

​ 他的意思就是说,在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清理掉

如下图。

1577885759697

​ 这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会 立即停止混合回收,意味着本次混合回收就结束了

​ 而且从这里也能看出来G1整体是基于复制算法进行Region垃圾回收的,不会出现内存碎片的问题,不需要像CMS那样标记-清理之后,再进行内存碎片的整理。

还有一个参数,“-XX:G1MixedGCLiveThresholdPercent”,他的默认值是85%,意思就是确定要回收的Region的时候,必须是存活对象低于85%的Region才可以进行回收

​ 否则要是一个Region的存活对象多余85%,你还回收他干什么?这个时候要把85%的对象都拷贝到别的Region,这个成本是很高的。

5、回收失败时的Full GC

​ 如果在进行Mixed回收的时候,无论是年轻代还是老年代都基于复制算法进行回收,都要把各个Region的存活对象拷贝到别的Region里去

​ 此时万一出现拷贝的过程中发现没有空闲Region可以承载自己的存活对象了,就会触发 一次失败。

​ 一旦失败,立马就会切换为停止系统程序,然后采用单线程进行标记、清理和压缩整理,空闲出来一批Region,这个过程是极慢极慢的。

总结

新生代触发GC的条件是:当新生代内存使用率达到%60(但是实际情况不会真的等到新生代内存用了60%,才去回收,G1会自动评估怎么在最短时间回收更多垃圾),那么就会触发GC,可以配合“-XX:MaxGCPauseMills”参数来控制GC的时间,也就是系统停止运行时间(STW)。

老年代触发GC条件:一般在老年代的Region占据了堆内存的Region的45%之后,会触发一个混合回收的过程,也就是Mixed GC,分为了好几个阶段你会发现G1的回收过程跟CMS回收器的步骤一样,也是四个步骤。

跟其他回收器的区别

(1)首先G1他是可以控制GC(region gc)时间。

(2)堆内存区域的划分,由eden+2个survivor,变成了最多2048个region区

(3)G1触发region gc的时机是,新生代超过了%60的内存。

(4)新生代对象进入老年代规则,依旧跟其他回收器一样

(5)G1大对象不会再进入老年代,而是通过region保存,如果一个region不够,可能会跨region保存。

ZGC垃圾回收器 - 未完待续

如果你感觉文章对你又些许感悟,你可以支持我!!
-------------本文结束感谢您的阅读-------------