垃圾回收算法g1
㈠ JVM G1参数
采用根搜索算法,通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
1. 栈中引⽤的对象
2. 静态变量、常量引⽤的对象
3. 本地⽅法栈native⽅法引⽤的对象
1.标记-复制
2.标记-清理
3.标记-整理
G1采取了不同的策略来解决并行、串行和CMS收集器的碎片、暂停时间不可控制等问题
G1会优先回收垃圾对象特别多的分区,这样可以花费较少的时间来回收这些分区的垃圾
在年轻代回收期间,G1 GC 会调整其年轻代空间(eden 和存活空间大小)以满足目标。
在混合回收期间,G1 GC 会根据混合垃圾回收的目标次数调整所回收的年老代区域数量,并调整堆的每个区域中存活对象的百分比,以及总体可接受的堆废物百分比。
G1算法将堆划分为若干个区域(Region), 每个region可以是edon, survior, old区域,每个region大小相同为1M, 2M, 4M 2的幂次方大小,整个堆中默认有2048个region,每个Region默认按照512Kb划分成多个Card。
如果一个对象占用的空间大于一个region尺寸的一半,就会专门放入到humongous区域。G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。这种情况可以调整整个堆的大小,或者调整G1HeapRegionSize大小。
Remembered Set ,对应于一个region,采用point-in策略,记录该region中某card被其它region 的引用情况。RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。 进行垃圾回收时,如果Region1有根对象A引用了Region2的对象B,显然对象B是活的,如果没有Rset,就需要扫描整个Region1或者其它Region,才能确定对象B是活跃的,有了Rset可以避免对整个堆进行扫描。
cms中年老代也有rset,采用point-out策略,记录年老代中引用年轻代的对象,这样在ygc时就不用扫描整个年老代,只扫描年老代的rset。
G1MaxNewSizePercent 新生代最大值,默认值60%
G1MaxPauseTime 设置G1收集过程目标时间,默认值200ms
G1ReservePercent 预留百分之多少内存,防止晋升失败的情况,默认值是10
-XX:=45 – 整个堆栈使用达到百分之多少的时候,启动GC周期. 基于整个堆,不仅仅是其中的某个代的占用情况,G1根据这个值来判断是否要触发GC周期, 0表示一直都在GC,默认值是45(即45%满了,或者说占用了),启动mix gc
MaxRAMPercentage、InitialRAMPercentage、MinRAMPercentage 应用于docker容器中,根据docker容器内存大小指定堆的初始,最大,最小比例
ParallelRefProcEnabled 默认为false,并行的处理Reference对象,如WeakReference,除非在GC log里出现Reference处理时间较长的日志,否则效果不会很明显
显式的使用-Xmn设置年轻代的大小,会干预G1的默认行为。
G1就不会再考虑设定的暂停时间目标,所以本质上说,设定了年轻代大小就相当于禁用了目标暂停时间。
G1就无法根据需要增大或者缩小年轻代的小心。既然大小固定了,就无法在大小上做任何改变了。
为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即mixed gc
除了回收整个young region,还会回收一部分的old region
主要分为以下几个步骤:
1. initial mark: 初始标记过程,整个过程STW,标记了从GC Root可达的对象
2. concurrent marking: 并发标记过程,整个过程gc collector线程与应用线程可以并行执行,标记出GC Root可达对象衍生出去的存活对象,并收集各个Region的存活对象信息
3. remark: 最终标记过程,整个过程STW,标记出那些在并发标记过程中遗漏的,或者内部引用发生变化的对象
4. clean up: 垃圾清除过程,如果发现一个Region中没有存活对象,则把该Region加入到空闲列表中
使用范围不一样
CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用
G1收集器收集范围是老年代和新生代
STW的时间
CMS收集器以最小的停顿时间为目标的收集器。
G1收集器可预测垃圾回收的停顿时间(建立可预测的停顿时间模型)
垃圾碎片
CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片
参考: https://www.jianshu.com/p/a3e6a9de7a5d
https://blog.csdn.net/u013380694/article/details/83341913
https://www.jianshu.com/p/ab54489f5d71?u_atoken=ca2d26ce-15a4-462b-9ee2-1d3dfca2d647&u_asession=-kRD0-eOdne8XcfWhUbbJUSLGdkUER_tKV6ZX0KNBwm7Lovlpxjd_P_q4JsKWYrT3W_NKPr8w6oU7K9iRp8G_&u_asig=_7vZwmCUEmowKET9soS-B3_YnquxJK1II_ufphdjR9EF5W4qBzbaQxa_DPpZ9KH_-QE6N5IgXkZa79JS7q8ZD7Xtz2Ly--WWPRPQyB_SKrj-61LB_f61u3h9VXwMyh6PgyDIVSG1W__la6lRJ-&u_aref=npKvxxWi1kDXXuw5mG2TYBN3CXA%3D
㈡ CMS 和G1 的区别
CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用
G1收集器收集范围是老年代和新生代。不需要结合其他收集器使用
CMS收集器以最小的停顿时间为目标的收集器。
G1收集器可预测垃圾回收的停顿时间(建立可预测的停顿时间模型)
CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。
初始标记
并发标记
重新标记
并发清理
初始标记阶段:会让线程全部停止,也就是 Stop the World 状态
并发标记阶段:对所有的对象进行追踪,这个阶段最耗费时。但这个阶段是和系统并发运行的,所以不会对系统运行造成影响
重新标记阶段:由于第二阶段是并发执行的,一边标记垃圾对象,一边创建新对象,老对象会变成垃圾对象。 所以第三阶段也会进入 Stop the World 状态,并且重新标记,标记的是第二阶段中变动过的少数对象,所以运行速度很快
并发清理阶段: 这个阶段也是会耗费很多时间,但由于是并发运行的,所以对系统不会造成很大的影响
CMS采用 标记-清理 的算法,标记出垃圾对象,清除垃圾对象。算法是基于老年代执行的,因为新生代产生无法接受该算法产生的碎片垃圾。
优点 :并发收集,低停顿
不足 :
G1的出现就是为了替换jdk1.5种出现的CMS,这一点已经在jdk9的时候实现了,jdk9默认使用了G1回收器,移除了所有CMS相关的内容。G1和CMS相比,有几个特点:
G1把java内存拆分成多等份,多个域(Region),逻辑上存在新生代和老年代的概念,但是没有严格区分
贴图感受一下:
依旧存在新生代老年代的概念,但是没有严格区分。Region最多分为2048个
除了上面优点之外,还有一个优点,那就是对大对象的处理。在CMS内存中,如果一个对象过大,进入S1、S2区域的时候大于改分配的区域,对象会直接进入老年代。G1处理大对象时会判断对象是否大于一个Region大小的50%,如果大于50%就会横跨多个Region进行存放
初始标记: 标记GC Roots 可以直接关联的对象,该阶段需要线程停顿但是耗时短
并发标记: 寻找存活的对象,可以与其他程序并发执行,耗时较长
最终标记: 并发标记期间用户程序会导致标记记录产生变动(好比一个阿姨一边清理垃圾,另一个人一边扔垃圾)虚拟机会将这段时间的变化记录在Remembered Set Logs 中。最终标记阶段会向Remembered Set合并并发标记阶段的变化。这个阶段需要线程停顿,也可以并发执行
筛选回收: 对每个Region的回收成本进行排序,按照用户自定义的回收时间来制定回收计划
参考官方文档:
控制G1回收垃圾的时间
-XX:MaxGCPauseMillis=200 (默认200ms)
㈢ JVM的垃圾算法有哪几种
一、垃圾收集器概述
如上图所示,垃圾回收算法一共有7个,3个属于年轻代、三个属于年老代,G1属于横跨年轻代和年老代的算法。
JVM会从年轻代和年老代各选出一个算法进行组合,连线表示哪些算法可以组合使用
二、各个垃圾收集器说明
1、Serial(年轻代)
年轻代收集器,可以和Serial Old、CMS组合使用
采用复制算法
使用单线程进行垃圾回收,回收时会导致Stop The World,用户进程停止
client模式年轻代默认算法
GC日志关键字:DefNew(Default New Generation)
图示(Serial+Serial Old)
7、G1
G1收集器由于没有使用过,所以从网上找了一些教程供大家了解
并行与并发
分代收集
空间整合
可预测的停顿
㈣ JVM之G1垃圾回收器
stop the world ,这个是最痛的一个点!无论是新生代垃圾回收,还是老年代垃圾回收,都会或多或少产生“stop the world”现象,对系统的运行时有一定影响的。所以其实之后对垃圾回收器的优化,都是朝着减少“stop the world”的目标去做的。在这个基础上,G1垃圾回收器就应运而生,它可以提供比 组合更好的垃圾回收的性能。
G1垃圾回收器是可以同时回收新生代和老年代的对象的,不需要两个垃圾回收器配合起来运作,它一个人就可以搞定所有的垃圾回收。它最大的一个特点,就是把java堆内存拆分为多个大小相等的Region ,如下图:
G1如果要做到这一点,它就必须要追踪每个Region里的回收价值,什么叫做 回收价值 ?它必须搞清楚每个Region里的对象有多少是垃圾,如果对这个Region进行垃圾回收,需要耗费多长时间,可以回收掉多少垃圾。
简单来说,G1可以做到让你来设定垃圾回收对系统的影响,它自己通过把内存拆分为大量小Region,以及追踪每个Region中可以回收的对象大小和预估时间,最后在垃圾回收的时候,尽量把垃圾回收对系统造成的影响控制在你指定的时间范围内,同时在有限的时间内尽量回收尽可能多的垃圾对象,这就是 G1的核心设计思路 。
刚开始Region可能谁都不属于,然后就分配给了新生代,放了很多属于新生代的对象,接着触发了垃圾回收这个Region,然后下一次这个Region可能又被分配给了老年代,用来放老年代长期存活的对象。所以G1对应的内存模型中,Region随时会属于新生代,也会属于老年代,所以没有所谓新生代给多少内存,老年代给多少内存这一说了。实际上新生代和老年代各自的内存区域是不停的变动的,由G1自动控制。
我们现在来思考两个问题:
1、到底有多少个Region?
2、每个Region的大小是多大呢?
其实默认情况下是自动计算和设置的,我们可以给整个堆内存设置一个大小,比如说用“-Xms”和“-Xmx”来设置堆内存的大小。然后jvm启动的时候一旦发现你使用的是G1垃圾回收器,可以使用“-XX:UserG1GC”来指定使用G1垃圾回收器,此时会自动用堆大小除以2048,因为jvm最多可以有2048个Region,然后Region的大小必须是2的倍数,比如说2MB、4MB之类的。大概就是这样子来决定Region的数量和大小的,大家一般保持默认的计算方式就可以。如果通过手动方式来指定,则可以通过“-XX:G1HeapRegionSize”参数来设置。
刚开始的时候,默认新生代堆内存的占比是5%,也就是占据200MB左右的内存,对应大概是100个Region,这个可以通过“-XX:G1NewSizePercent”来设置新生代的初始占比,其实维持这个默认值即可。因为在系统运行中,jvm其实会不停的给新生代增加更多的Region,但是新生代的占比最多不会超过60%,但是可以通过“-XX:G1MaxNewSizePercent”参数来调整比例。而且一旦Region进行了垃圾回收,此时新生代的Region数量还会减少,这些其实都是动态的。
其实在G1中虽然把内存划分为了很多的Region,但还是有新生代、老年代的区分。而且新生代里还是有Eden和Survivor的划分的,所以大家会发现之前学习的很多技术原理在G1时期都是有用的。大家应该还记得之前说过的一个新生代的参数,“-XX:SurvivorRatio=8”,所以这里还是可以区分出来属于新生代的Region里哪些属于Eden区,哪些属于Survivor区。比如之前说新生代刚开始的时候,有100个Region,那么可能80个Region就是Eden区,两个Survivor区各自占10个Region。因为新生代的Region数量是动态的,所以随着对象不停的在新生代里分配,属于新生代的Region会不断增加,Eden和Survivor对应的Region也会不断增加。
既然G1的新生代也有Eden和Survivor的区分,那么触发垃圾回收的机制都是类似的。随着不停的在新生代的Eden区对应的Region中放对象,jvm就会不停的给新生代加入更多的Region,直到新生代占据堆大小的最大比例60%为止。一旦新生代达到了设定的占据堆内存的最大大小60%,这个时候就会触发新生代的GC,G1就会用之前说过的复制算法来进行垃圾回收,进入一个“stop the world”状态,然后把Eden区对应的Region中的存活对象放入S1区对应的Region中,接着回收掉Eden区对应的Region中的垃圾对象。 ,因为G1是可以设定目标GC停顿时间的,也就是G1执行GC的时候最多可以让系统停顿多长时间,可以通过“-XX:MaxGCPauseMills”参数来设定,默认值是200ms,那么在程序运行期间G1会根据你设定的gc停顿时间给新生代不停分配Region,然后到一定程度,就会触发新生代gc,保证新生代gc的时候导致的系统停顿时间在你预设的范围内,当然这个数字并不是那么的精准。
我们都知道,在G1的内存模型下,新生代和老年代各自都会占据一定的Region,老年代也会有自己的Region。按照默认新生代最大只能占据堆内存60%的Region来推算,老年代最多可以占据40%的Region,大概就是800个左右的Region。
可以说跟之前几乎是一样的,还是这么几个条件:
1、对象在新生代躲过了很多次的垃圾回收,达到了一定的年龄了它就会进入老年代,“-XX:MaxTenuringThreshold”参数可以设置这个年龄。
2、动态年龄判定规则,如果一旦发现某次新生代GC过后,存活对象超过了Survivor的50%,此时就会判断一下,比如年龄为1岁,2岁,3岁,4岁的对象的大小总和超过了Survivor的50%,此时4岁以上的对象全部会进入老年代,这就是动态年龄判定规则。
以前说是那种大对象也是可以直接进入老年代的,那么现在在G1这套内存模型下呢?实际上这里会有所改变,G1提供了专门的Region来存放大对象,而不是让大对象进入老年代中的Region。在G1中,大对象的判断规则就是一个大对象超过了一个Region大小的50%,而且一个大对象如果太大,可能会横跨多个Region来存放。 不是说60%给新生代,40%给老年代吗,那还有Region给大对象?我们现在知道,在G1里,新生代和老年代的Region是不停变化的。比如新生代现在占据了1200个Region,但是一次垃圾回收之后,就让里面1000个Region都空了,此时那1000个Region就可以不属于新生代了,里面很多Region可以用来存放大对象。那么大对象既然不属于新生代和老年代,那么什么时候会触发垃圾回收呢?其实新生、老年代在回收的时候,会顺带带着大对象Region一起回收,所以,这就是在G1内存模型下对大对象的分配和回收的策略。
G1有一个参数是“-XX:”,它的默认值是45%,意思是说,如果老年代占据了堆内存的45%的Region的时候,此时会尝试触发一次新生代+老年代一起回收的混合回收阶段。
,这个过程是需要进入“stop the world”的,但这个过程仅仅只是标记一下GC Roots直接能引用的对象,所以速度是很快的。 ,这个阶段会允许系统程序运行,同时进行GC Roots追踪,从GC Roots开始追踪所有的存活对象,这个过程加入了对间接引用对象的追踪,这个过程前面的文章已经介绍过了,这里就不再赘述。 ,这个阶段会进入“stop the world”,系统程序会禁止运行,最终标记一下有哪些是存活对象,有哪些是垃圾对象。 ,这个阶段会计算老年代中每个Region中的存活对象数量,存活对象占比,还有执行垃圾回收的预期性能和效率。接着会停止系统程序,然后全力以赴进行垃圾回收,此时会选择部分Region进行回收,因为必须让垃圾回收的停顿时间控制在我们指定的范围内,所以说它会从新生代、老年代、大对象里各自挑选一些Region,保证用指定的时间回收尽可能多的垃圾,这就是所谓的混合回收。
G1垃圾回收器的一些参数
最后一个阶段混合回收的时候,会停止所有程序运行,所以说G1是允许执行多次混合回收。比如先停止工作,执行一次混合回收,回收掉一些Region,接着恢复系统运行,然后再次停止系统运行,再执行一次混合回收,回收掉一些Region。有一些参数可以控制这个,比如“-XX:G1MixedGCCountTarget”参数,就是一次混合回收的过程中,最后一个阶段执行几次混合回收,默认值是8次。这样的好处是不让系统停止的时间过长。还有一个参数就是“-XX:G1HeapWastePercent”,默认值是5%,它的意思是说,在回收的过程中会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立即停止混合回收,意味着本次混合回收就结束了。
G1整体是基于复制算法进行Region垃圾回收的,不会出现内存碎片的问题,不需要像CMS那样标记-清理之后再进行内存碎片的整理。
还有一个参数,“-XX:G1MixedGCLiveThresholdPercent”,它的默认值是85%,意思是确定要回收的Region的时候,必须是存活对象低于85%的Region才可以进行回收,否则要是一个Region的存活对象多余85%,回收它也作用不大,而且还要把85%的对象都拷贝到别的Region,这个成本是很高的。
万一出现拷贝的过程中发现没有空闲Region可以承载自己的存活对象了,就会触发一次失败。一旦失败,立马就会切换为停止系统程序,然后采用单线程进行标记、清理和压缩整理,空闲出来一批Region,这个过程是极慢的。
G1垃圾回收器的使用场景
当你的系统部署在大内存机器上的时候,比如说你的机器是32核64G的机器,此时你分配给系统的内存有几十个G,新生代的Eden区可能30~40G的内存。比如类似kafka、elasticsearch之类的大数据相关系统,都是部署在大内存的机器上的。此时如果你的系统负载非常的高,比如每秒几万的访问请求到kafka、elasticsearch上去,那么可能导致你Eden区的几十G内存频繁塞满,然后要触发垃圾回收,假设1分钟会塞满一次。然后每次垃圾回收要停掉kafka、elasticsearch的运行,然后执行垃圾回收大概需要几秒钟,此时你发现,可能每过一分钟,你的系统就要卡顿几秒钟,有的请求一旦卡死几秒钟就会超时报错,此时可能会导致你的系统频繁出错。 !针对G1垃圾回收器,我们可以设置每次GC的停顿时间,比如我们设置100ms,那么每次垃圾回收我们的系统最多也就停顿100ms,然后系统继续运行。G1天生就适合这种大内存机器的jvm运行。
本文结束。
㈤ 面试官:谈谈你对G1垃圾收集器有哪些了解
作为一款高效的垃圾收集器,G1在JDK7中加入JVM,在JDK9中取代CMS成为了默认的垃圾收集器。
新生代采用复制算法,主要的垃圾收集器有三个,Serial、Parallel New 和 Parallel Scavenge,特性如下:
G1垃圾收集器主要用于多处理器、大内存的场景,它有五个属性:分代、增量、并行(大多时候可以并发)、stop the word、标记整理。
我们知道,垃圾收集器的一个目标就是STW(stop the word)越短越好。利用可预测停顿时间模型,G1为垃圾收集设定一个STW的目标时间(通过 -XX:MaxGCPauseMillis 参数设定,默认200ms),G1尽可能地在这个时间内完成垃圾收集,并且在不需要额外配置的情况下实现高吞吐量。
G1致力于在下面的应用和环境下寻找延迟和吞吐量的最佳平衡:
如果在JDK8中使用G1,我们可以使用参数 -XX:+UseG1GC 来开启。
G1把整个堆分成了大小相等的region,每一个region都是连续的虚拟内存,region是内存分配和回收的基本单位。如下图:
红色带"S"的region表示新生代的survivor,红色不带"S"的表示新生代eden,浅蓝色不带"H"的表示老年代,浅蓝色带"H"的表示老年代中的大对象。跟G1之前的内存分配策略不同的是,survivor、eden、老年代这些区域可能是不连续的。
G1在停顿的时候可以回收整个新生代的region,新生代region的对象要不复制到survivor区要不复制到老年代region。同时每次停顿都可以回收一部分老年代的内存,把老年代从一个region复制到另一个region。
上一节我们看到,整个堆内存被G1分成了多个大小相等的region,每个堆大约可以有2048个region,每个region大小为 1~32 MB(必须是2的次方)。region的大小通过 -XX:G1HeapRegionSize 来设置,所以按照默认值来G1能管理的最大内存大约 32MB * 2048 = 64G。
大对象是指大小超过了region一半的对象,大对象可以横跨多个region,给大对象分配内存的时候会直接分配在老年代,并不会分配在eden区。
如下图,一个大对象占据了两个半region,给大对象分配内存时,必须从一个region开始分配连续的region,在大对象被回收前,最后一个region不能被分配给其他对象。
大对象什么时候回收 ?通常,只有在mark结束以后的Cleanup停顿阶段或者FullGC的时候,死亡的大对象才会被回收掉。但是,基本类型(比如bool数组、所有的整形数组、浮点型数组等)的数组大对象有个例外,G1会在任何GC停顿的时候回收这些死亡大对象。这个默认是开启的,但是可以使用
-XX: 这个参数禁用掉。
分配大对象的时候,因为占用空间太大,可能会过早发生GC停顿。G1在每次分配大对象的时候都会去检查当前堆内存占用是否超过初始堆占用阈值IHOP(The Initiating Heap Occupancy Percent),如果当前的堆占用率超过了IHOP阈值,就会立刻触发 initial mark。 关于initial mark详见第4节 。
即使是在FullGC的时候,大对象也是永远不会被移动的。这可能导致过早发生FullGC或者是意外的OOM,因为此时虽然还有大量的空闲内存,但是这些内存都是region中的内存碎片。
G1虽然把堆内存划分成了多个region,但是依然存在新生代和老年代的概念。G1新增了2个控制新生代内存大小的参数,-XX:G1NewSizePercent(默认等于5),-XX:G1MaxNewSizePercent(默认等于60)。也就是说新生代大小默认占整个堆内存的 5% ~ 60%。
根据前面介绍,一个堆大概可以分配2048个region,每个region最大32M,这样G1管理的整个堆的大小最大可以是64G,新生代占用的大小范围是 3.2G ~ 38.4G。
对于 -XX:G1NewSizePercent 和 -XX:G1MaxNewSizePercent,下面几个问题需要注意:
生效,比如堆大小是64G,设置 -Xmn3.2G,那么就等价于 -XX:G1NewSizePercent=5 并且 -XX:G1MaxNewSizePercent=5,因为3.2G/64G = 5%。
生效,比如堆大小是64G,设置 -XX:NewRatio=3,那么就等价于 -XX:G1NewSizePercent=25 并且 -XX:G1MaxNewSizePercent=25。因为年轻代:老年代 = 1 :3,说明年轻代占1/4 = 25%。
设置的这个参数不生效,两个参数都用默认值。
跟 -XX:GCTimeRatio 这个参数相关。这个参数为0~100之间的整数(G1默认是9, 其它收集器默认是99),值为 n 则系统将花费不超过 1/(1+n) 的时间用于垃圾收集。因此G1默认最多 10% 的时间用于垃圾收集,如果垃圾收集时间超过10%,则触发扩容。如果扩容失败,则发起Full GC。
G1的垃圾收集是在 Young-Only 和 Space-Reclamation两个阶段交替执行的。如下图:
young-only阶段会用对象逐步把老年代区域填满,space-reclamation阶段除了会回收年轻代的内存以外,还会增量回收老年代的内存。完成后重新开始young-only阶段。
Young-only阶段流程如下图:
这个阶段从普通的 young-only GC 开始,young-only GC把一些对象移动到老年代,当老年代的空间占用达到IHOP时,G1就停止普通的young-only GC,开始初始标记(Initial Mark)。
在young-only阶段,要回收新生代的region。每一次 young-only 结束的时候,G1总是会调整新生代大小。G1可以使用参数 -XX:MaxGCPauseTimeMillis和
-XX:PauseTimeIntervalMillis 来设置目标停顿时间,这两个参数是对实际停顿时间的长期观察得来的。他会根据在GC的时候要拷贝多少个对象,对象之间是如何相互关联的等信息计算出来回收相同大小的新生代内存需要花费多少时间,
如果没有其他的限定条件,G1会把young区的大小调整为 -XX:G1NewSizePercent和 -XX:G1MaxNewSizePercent 之间的值来满足停顿时间的要求。
这个阶段由多个Mixed GC组成,不光回收年轻代垃圾,也回收老年代垃圾。当 G1 发现回收更多的老年代区域不能释放更多空闲空间时,这个阶段结束。之后,周期性地再次开启一个新的Young-only阶段。
当G1收集存活对象信息时内存不足,G1会做一个Full GC,并且会STW。
在 space-reclamation 阶段,G1会尽量在GC停顿时间内回收尽可能多的老年代内存。这个阶段新生代内存大小被调整为 -XX:G1NewSizePercent 设置的允许的最小值,只要存在可回收的老年代region就会被添加到回收集合中,直到再添加会超出目标停顿时间为止。在特定的某个GC停顿时间内,G1会按照这老年代region回收的效率(效率高的优先收集)和剩余可用时间来得到最终待回收region集合。
每一个GC停顿期间要回收的老年代region数量受限于候选region集合数量除以 -XX:G1MixedGCCountTarget 这个参数值,参数 -XX:G1MixedGCCountTarget 指定一个周期内触发Mixed GC最大次数,默认值8。比如 -XX:G1MixedGCCountTarget 采用默认值8,候选region集合有200个region,那每次停顿期间收集25个region。
当待回收region集合中可回收的空间占用率低于参数值 -XX:G1HeapWastePercent 的时候,Space-Reclamation结束。
当应用存活对象占用了大量内存,以至于回收剩余对象没有足够的空间拷贝时,就会触发 evacuation failure。这时G1为了完成当前的垃圾收集,会保留已经位于新的位置上的存活对象不动,对于没有移动和拷贝的对象就不会进行拷贝了,仅仅调整对象间的引用。
evacuation failure会导致一些额外的开销,但是一般会跟其他 young GC 一样快。evacuation failure完成以后,G1会跟正常情况下一样继续恢复应用的执行。G1会假设 evacuation failure是发生在GC的后期,这时大部分对象已经移动过了,并且已经有足够的内存来继续执行应用程序一直到 mark 结束 space-reclamation 开始。如果这个假设不成立(也就是说没有足够的内存来执行应用程序),G1最终只能发起Full GC,对整个堆做压缩,这个过程可能会非常慢。
Parallel GC 可以压缩和回收老年代的内存,但是也只能对老年代整体来操作。G1以增量的方式把整个GC工作增量的分散到多个更短的停顿时间中,当然这可能会牺牲一定吞吐量。
跟CMS类似,G1并发回收老年代内存,但是,CMS采用标记-清除算法,不会处理老年代的内存碎片,最终就会导致长时间的FullGC。
因为采用并发收集,G1的性能开销会更大,这可能会影响吞吐量。
G1在任何的GC期间都可以回收老年代中全空或者占用大空间的内存。这可以避免一些不必要的GC,因为可以非常轻易地释放大量的内存空间。这个功能默认开启,可以采用
-XX:- 参数关闭。
G1可以选择对整个堆里面的String进行并行去重。这个功能默认关闭,可以使用参数 -XX:+
G1EnableStringDeplication 来开启。
本文详细介绍了G1垃圾收集器,希望能够对你理解G1有所帮助。
㈥ G1从入门到放弃(一)
最近在看关于G1垃圾收集的文章,看了很多国内与国外的资料,本文对G1的这些资料进行了整理。这篇合适JVM垃圾回收有一定基础的同学,作为G1入门可以看一下,如果要死磕G1实现的内容细节。大家可以找 R大 。 个人认为R大是目前国内JVM领域研究的先驱了,当然R大也是不建议大家去看JVM的源码的。 为啥别读HotSpot VM的源码
G1系列第一篇文章会介绍G1的理论知识,不会做JVM源码的深入分析。第二篇准备介绍G1实践中的日志分析。
G1(Garbadge First Collector)作为一款JVM最新的垃圾收集器,可以解决CMS中Concurrent Mode Failed问题,尽量缩短处理超大堆的停顿,在G1进行垃圾回收的时候完成内存压缩,降低内存碎片的生成。G1在堆内存比较大的时候表现出比较高吞吐量和短暂的停顿时间,而且已成为Java 9的默认收集器。未来替代CMS只是时间的问题。
G1的内存结构和传统的内存空间划分有比较的不同。G1将内存划分成了多个大小相等的Region(默认是512K),Region逻辑上连续,物理内存地址不连续。同时每个Region被标记成E、S、O、H,分别表示Eden、Survivor、Old、Humongous。其中E、S属于年轻代,O与H属于老年代。
示意图如下:
H表示Humongous。从字面上就可以理解表示大的对象(下面简称H对象)。 当分配的对象大于等于Region大小的一半 的时候就会被认为是巨型对象。H对象默认分配在老年代,可以防止GC的时候大对象的内存拷贝。通过如果发现堆内存容不下H对象的时候,会触发一次GC操作。
在进行Young GC的时候,Young区的对象可能还存在Old区的引用, 这就是跨代引用的问题。为了解决Young GC的时候,扫描整个老年代,G1引入了 Card Table 和 Remember Set 的概念,基本思想就是用空间换时间。这两个数据结构是专门用来处理Old区到Young区的引用。Young区到Old区的引用则不需要单独处理,因为Young区中的对象本身变化比较大,没必要浪费空间去记录下来。
下图展示的是 RSet 与 Card 的关系。每个 Region 被分成了多个 Card ,其中绿色部分的 Card 表示该 Card 中有对象引用了其他 Card 中的对象,这种引用关系用蓝色实线表示。 RSet 其实是一个HashTable,Key是Region的起始地址,Value是 Card Table (字节数组),字节数组下标表示 Card 的空间地址,当该地址空间被引用的时候会被标记为 dirty_card 。
关于RSet结构的维护,可以参考这篇 文章 ,这里不做过多的深入。
SATB的全称(Snapshot At The Beginning)字面意思是开始GC前存活对象的一个快照。SATB的作用是保证在并发标记阶段的正确性。如何理解这句话?
首先要介绍三色标记算法。
在GC扫描C之前的颜色如下:
在并发标记阶段,应用线程改变了这种引用关系
得到如下结果。
在重新标记阶段扫描结果如下
这种情况下C会被当做垃圾进行回收。Snapshot的存活对象原来是A、B、C,现在变成A、B了,Snapshot的完整遭到破坏了,显然这个做法是不合理。
G1采用的是 pre-write barrier 解决这个问题。简单说就是在并发标记阶段,当引用关系发生变化的时候,通过 pre-write barrier 函数会把这种这种变化记录并保存在一个队列里,在JVM源码中这个队列叫 satb_mark_queue 。在remark阶段会扫描这个队列,通过这种方式,旧的引用所指向的对象就会被标记上,其子孙也会被递归标记上,这样就不会漏标记任何对象,snapshot的完整性也就得到了保证。
这里引用R大对SATB的解释:
SATB的方式记录活对象,也就是那一时刻对象snapshot, 但是在之后这里面的对象可能会变成垃圾, 叫做浮动垃圾(floating garbage),这种对象只能等到下一次收集回收掉。在GC过程中新分配的对象都当做是活的,其他不可达的对象就是死的。
如何知道哪些对象是GC开始之后新分配的呢?
在Region中通过top-at-mark-start(TAMS)指针,分别为prevTAMS和nextTAMS来记录新配的对象。示意图如下:
每个region记录着两个top-at-mark-start(TAMS)指针,分别为prevTAMS和nextTAMS。在TAMS以上的对象就是新分配的,因而被视为隐式marked。 这里引用R大的解释。
其中top是该region的当前分配指针,[bottom, top)是当前该region已用(used)的部分,[top, end)是尚未使用的可分配空间(unused)。
(1): [bottom, prevTAMS): 这部分里的对象存活信息可以通过prevBitmap来得知
(2): [prevTAMS, nextTAMS): 这部分里的对象在第n-1轮concurrent marking是隐式存活的
(3): [nextTAMS, top): 这部分里的对象在第n轮concurrent marking是隐式存活的
Young GC 回收的是所有年轻代的Region。 当E区不能再分配新的对象时就会触发 。E区的对象会移动到S区,当S区空间不够的时候,E区的对象会直接晋升到O区,同时S区的数据移动到新的S区,如果S区的部分对象到达一定年龄,会晋升到O区。
Yung GC过程示意图如下:
Mixed GC 翻译过来叫混合回收。之所以叫混合是因为回收所有的年轻代的Region+部分老年代的Region。
1、为什么是老年代的 部分 Region?
2、什么时候触发Mixed GC?
这两个问题其实可以一并回答。回收 部分 老年代是参数 -XX:MaxGCPauseMillis ,用来指定一个G1收集过程目标停顿时间,默认值200ms,当然这只是一个期望值。G1的强大之处在于他有一个停顿预测模型(Pause Prediction Model),他会有选择的挑选 部分 Region,去尽量满足停顿时间,关于G1的这个模型是如何建立的,这里不做深究。
Mixed GC的触发也是由一些参数控制。比如 XX: 表示老年代占整个堆大小的百分比,默认值是45%,达到该阈值就会触发一次Mixed GC。
Mixed GC主要可以分为两个阶段:
1、全局并发标记(global concurrent marking)
全局并发标记又可以进一步细分成下面几个步骤:
2、拷贝存活对象(Evacuation)
Evacuation阶段是全暂停的。它负责把一部分region里的活对象拷贝到空region里去(并行拷贝),然后回收原本的region的空间。Evacuation阶段可以自由选择任意多个region来独立收集构成收集集合(collection set,简称CSet),CSet集合中Region的选定依赖于上文中提到的 停顿预测模型 ,该阶段并不evacuate所有有活对象的region,只选择收益高的少量region来evacuate,这种暂停的开销就可以(在一定范围内)可控。
Mixed GC的清理过程示意图如下:
G1的垃圾回收过程是和应用程序并发执行的,当Mixed GC的速度赶不上应用程序申请内存的速度的时候,Mixed G1就会降级到Full GC,使用的是Serial GC。Full GC会导致长时间的STW,应该要尽量避免。
导致G1 Full GC的原因可能有两个:
PS: 本文主要参考的国内文章:
java Hotspot G1 GC的一些关键技术
Garbage First G1收集器 理解和原理分析
G1: One Garbage Collector To Rule Them All
请教G1算法的原理
深入理解 Java G1 垃圾收集器
Getting Started with the G1 Garbage Collector !
㈦ jvm的理解
1
JVM内存区域
我们在编写程序时,经常会遇到OOM(out of Memory)以及内存泄漏等问题。为了避免出现这些问题,我们首先必须对JVM的内存划分有个具体的认识。JVM将内存主要划分为:方法区、虚拟机栈、本地方法栈、堆、程序计数器。JVM运行时数据区如下:
1.1
程序计数器
程序计数器是线程私有的区域,很好理解嘛~,每个线程当然得有个计数器记录当前执行到那个指令。占用的内存空间小,可以把它看成是当前线程所执行的字节码的行号指示器。如果线程在执行Java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址;如果执行的是Native方法,这个计数器的值为空(Undefined)。
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
1.2
Java虚拟机栈
与程序计数器一样,Java虚拟机栈也是线程私有的,其生命周期与线程相同。
如何理解虚拟机栈呢?
本质上来讲,就是个栈。里面存放的元素叫栈帧,栈帧好像很复杂的样子,其实它很简单!它里面存放的是一个函数的上下文,具体存放的是执行的函数的一些数据。执行的函数需要的数据无非就是局部变量表(保存函数内部的变量)、操作数栈(执行引擎计算时需要),方法出口等等。
执行引擎每调用一个函数时,就为这个函数创建一个栈帧,并加入虚拟机栈。换个角度理解,每个函数从调用到执行结束,其实是对应一个栈帧的入栈和出栈。
注意这个区域可能出现的两种异常:
一种是StackOverflowError,当前线程请求的栈深度大于虚拟机所允许的深度时,会抛出这个异常。制造这种异常很简单:将一个函数反复递归自己,最终会出现栈溢出错误(StackOverflowError)。
另一种异常是OutOfMemoryError异常,当虚拟机栈可以动态扩展时(当前大部分虚拟机都可以),如果无法申请足够多的内存就会抛出OutOfMemoryError,如何制作虚拟机栈OOM呢,参考一下代码:
这段代码有风险,可能会导致操作系统假死,请谨慎使用~~~
1.3
本地方法栈
本地方法栈与虚拟机所发挥的作用很相似,他们的区别在于虚拟机栈为执行Java代码方法服务,而本地方法栈是为Native方法服务。与虚拟机栈一样,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。
1.4
Java堆
Java堆可以说是虚拟机中最大一块内存了。它是所有线程所共享的内存区域,几乎所有的实例对象都是在这块区域中存放。当然,随着JIT编译器的发展,所有对象在堆上分配渐渐变得不那么“绝对”了。
Java堆是垃圾收集器管理的主要区域。由于现在的收集器基本上采用的都是分代收集算法,所有Java堆可以细分为:新生代和老年代。在细致分就是把新生代分为:Eden空间、From Survivor空间、To Survivor空间。当堆无法再扩展时,会抛出OutOfMemoryError异常。
1.5
方法区
方法区存放的是类信息、常量、静态变量等。方法区是各个线程共享区域,很容易理解,我们在写Java代码时,每个线程度可以访问同一个类的静态变量对象。由于使用反射机制的原因,虚拟机很难推测那个类信息不再使用,因此这块区域的回收很难。另外,对这块区域主要是针对常量池回收,值得注意的是JDK1.7已经把常量池转移到堆里面了。同样,当方法区无法满足内存分配需求时,会抛出OutOfMemoryError。
制造方法区内存溢出,注意,必须在JDK1.6及之前版本才会导致方法区溢出,原因后面解释,执行之前,可以把虚拟机的参数-XXpermSize和-XX:MaxPermSize限制方法区大小。
运行后会抛出java.lang.OutOfMemoryError:PermGen space异常。
解释一下,String的intern()函数作用是如果当前的字符串在常量池中不存在,则放入到常量池中。上面的代码不断将字符串添加到常量池,最终肯定会导致内存不足,抛出方法区的OOM。
下面解释一下,为什么必须将上面的代码在JDK1.6之前运行。我们前面提到,JDK1.7后,把常量池放入到堆空间中,这导致intern()函数的功能不同,具体怎么个不同法,且看看下面代码:
这段代码在JDK1.6和JDK1.7运行的结果不同。
JDK1.6结果是:false,false ,JDK1.7结果是true, false。
原因是:JDK1.6中,intern()方法会吧首次遇到的字符串实例复制到常量池中,返回的也是常量池中的字符串的引用,而StringBuilder创建的字符串实例是在堆上面,所以必然不是同一个引用,返回false。
在JDK1.7中,intern不再复制实例,常量池中只保存首次出现的实例的引用,因此intern()返回的引用和由StringBuilder创建的字符串实例是同一个。为什么对str2比较返回的是false呢?这是因为,JVM中内部在加载类的时候,就已经有"java"这个字符串,不符合“首次出现”的原则,因此返回false。