转载、引用请标明出处
//www.greatytc.com/p/35805f809a21
本文出自zhh_happig的简书博客,谢谢
以下内容,是本人学习的笔记和工作中的总结,仅供大家参考,有误的地方还请指正
一 G1简介
-
JDK7增加,成为HotSpot重点发展的垃圾回收技术,被HotSpot团队寄予取代CMS的使命,将会被安排成为JDK9的默认垃圾收集器
-
低停顿(stw)、高吞吐;调优简单,设置2个参数
- 最基本设置堆大小和最大stw时间
- java -Xmx32G -xx:MaxGCPauseMillis=100 ...
- G1的最大stw时间默认是250ms
- G1的还有很多调优参数
- 对比CMS,G1的调优要简单的多
-
G1是一种分代收集器,只有逻辑上的分代概念,与物理上分代有本质区别
- 年轻代:采用复制算法
- 年老代:标记-清楚算法,类似CMS
-
G1的特点
- G1的设计原则是"首先收集尽可能多的垃圾(Garbage First)"。G1并不会等内存耗尽(串行、并行)或者快耗尽(CMS)的时候开始垃圾收集,而是在老年代找出具有高收集收益的分区进行收集。同时G1可以根据用户设置的暂停时间自动调整年轻代和总堆大小,暂停越短年轻代空间越小、总空间就越大
- G1采用内存分区(Region)的思路,回收时则以分区为单位进行回收,存活的对象复制到另一个空闲分区中。由于都是以相等大小的分区为单位进行操作,因此G1天然就是一种压缩方案(局部压缩)
- G1的收集都是STW的,采用了混合(mixed)收集的方式。即每次收集既可能只收集年轻代分区(年轻代收集),也可能在收集年轻代的同时,包含部分年老代分区(混合收集),这样即使堆内存很大时,也可以限制收集范围,从而降低停顿
-
G1有详细的日志信息,建议使用下面的参数,当G1出问题,可以获取很多有用的信息
- -xx:+PrintGCDateStamps 打印日期和正常云行时间
- -xx:+PrintGCDetails 打印G1详细信息
- -xx:+PrintAdaptiveSizePolicy 打印自适应调节策略;自适应策略:GC会根据中统计的GC时间、吞吐量、内存占用量,重新计算堆内存中各区大小
- -xx:+PrintTenuringDistribution 打印survivor region区域内的对象的age信息
二 G1内存布局
-
G1整个堆空间分成若干个大小相等的内存区域-Regions
-
默认将整堆划分为2048个分区,可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂)
-
Regions分为
- Eden Regions
- Survivor Regions
- Old Regions
- Humongous Regions
- 当一个对象大于Region大小的50%,称为巨型对象;它就会独占一个或多个Region,巨型对象会直接在老年代分配,所占用的连续空间称为巨型分区-Humongous Region
-
Card
- 很小的内存区域,G1将Java堆划分为相等大小的一个个区域,这个小的区域大小是512 Byte,称为Card
- Card Table维护着所有的Card。Card Table的结构是一个字节数组,Card Table用这个数组映射着每一个Card
- Card中对象的引用发生改变时,Card在Card Table数组中对应的值被标记为dirty,就称这个Card被脏化了
- 分配对象会占用物理上连续若干个卡片
-
G1保留了分代的概念,但是年轻代和年老代不再是物理上的隔离,他们都是一部分的Regions(不需要连续)的集合,每个Region都可能随G1的运行在不同代之间切换
- 年轻代空间并不是固定不变的,当现有年轻代分区占满时,JVM会分配新的空闲Region加入到年轻代空间。
- 整个年轻代内存会在初始空间-XX:G1NewSizePercent(默认整堆5%)与最大空间-XX:G1MaxNewSizePercent(默认60%)之间动态变化,且由参数目标暂停时间-XX:MaxGCPauseMillis(默认200ms)、需要扩缩容的大小以及分区的已记忆集合(RSet)计算得到。当然,G1依然可以设置固定的年轻代大小(参数-XX:NewRatio、-Xmn),但同时暂停目标将失去意义。
-
TLAB
- G1中每个线程本地的内存分配不需要顾及分区是否连续
三 G1工作解析
1 分配内存
- 当jvm开始运行时,堆内存开始分区,分成若干个大小相等的Regions
- 新的对象被分配到Eden Region,一个Eden Region满了之后,分配下一个Eden Region
- 当所有的Eden Regions都满了,就进行一次Young GC
-
Old Region中的对象也会指向了Eden Region中的对象:例如 一个"old" Map 存放进了 一个 "new" 的对象
- 上图中B指向了E,但是没有其他对象指向B了,显然B和E都是垃圾对象
- Young GC只会清理Eden Region,发现E有引用指向它,就不会去回收E,G1必须追踪这些内部区域之间的引用指向,才能正确的标记存活对象,回收垃圾对象
-
Old Region中的对象也会指向了Eden Region中的对象:例如 一个"old" Map 存放进了 一个 "new" 的对象
- 进过Young GCEden之后,Eden分区存活的对象将被拷贝到Survivor分区;原有Survivor分区存活的对象,将根据对象的年龄而晋升到新的survivor分区和老年代分区
2 G1扫描标记对象
-
Card Table:
- Card Table维护着所有的Card。Card Table的结构是一个字节数组,Card Table用这个数组映射着每一个Card
- Card中对象的引用发生改变时,Card在Card Table数组中对应的值被标记为dirty,就称这个Card被脏化了
- 所以Card Table其实就是映射着内存中的对象,Young GC的时候只需要扫描状态是dirty的card
-
Remembered Set: RSet
- 每一个Region都有自己的RSet
- RSet里面记录了引用——就是其他Region中指向本Region中所有对象的所有引用,也就是谁引用了我的对象
- RSet其实是一个Hash Table,Key是其他的Region的起始地址,Value是一个集合,里面的元素是Card Table 数组中的index,既Card对应的Index,映射到对象的Card地址。
- 比如A对象在regionA,B对象在regionB,且B.f = A,则在regionA的RSet中需要记录一对键值对,key是regionB的起始地址,Value的值能映射到B所在的Card的地址,所以要查找B对象,就可以通过RSet中记录的卡片来查找该对象
-
本分区对象引用本分区自己的对象,这种引用不用落入RSet中;同时,G1 GC每次都会对年轻代进行整体收集,因此young->old和young->young也不需要在RSet中记录。而对于old->young和old->old的跨代对象引用,需要拥有RSet
-
G1进行GC时,只要扫描本Region中RSet所记录的引用指向的对象是否存活,进而确定本分区内的对象存活情况。而不需要扫描整个堆了。
3 三色标记算法
-
并发标记中的三色标记算法,将对象分成三种类型
- 黑色: 根对象,或者该对象与它的子对象都被扫描
- 灰色: 对象本身被扫描,但还没扫描完该对象中的子对象
- 白色: 未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象
-
三色标记过程
-
当GC开始扫描对象时,按照如下图步骤进行对象的扫描:根对象被置为黑色,往下扫描,扫描子对象,子对象被置为灰色,如下图
-
继续由灰色对象往下扫描,扫描子对象,子对象被置为灰色,将已扫描完子对象的对象置为黑色
-
遍历了所有可达的对象后,所有可达的对象都变成了黑色。绝对不可能有黑对象指向白对象的情况发生。白色对象需要被清理
-
当GC开始扫描对象时,按照如下图步骤进行对象的扫描:根对象被置为黑色,往下扫描,扫描子对象,子对象被置为灰色,如下图
-
并发标记引起的对象丢失问题
- 在标记的同时应用程序也在运行,对象的引用随时会改变。当垃圾收集器扫描到下面情况时:
-
当垃圾收集器扫描到下图时,应用程序执行了A.c=C;B.c=null;
-
那么对象之间的引用就会如下图
-
当垃圾收集器再标记扫描的时候就会变成下图这样
- 那么此时C是白色的,会被认为是垃圾需要清理掉,显然这是不合理的
-
并发标记引起的对象丢失问题在CMS和G1的2种不同解决方式
-
栅栏 Barrier:栅栏是指在原生代码片段中,当某些语句被执行时,栅栏代码也会被执行。而G1主要在赋值语句中,使用写前栅栏(Pre-Write Barrrier)和写后栅栏(Post-Write Barrrier)。
- 写前栅栏 Pre-Write Barrrier:即将执行一段赋值语句时,等式左侧对象将修改引用到另一个对象,那么JVM就需要在赋值语句生效之前,记录丧失引用的对象。
- 写后栅栏 Post-Write Barrrier:当执行一段赋值语句后,等式右侧对象获取了左侧对象的引用,同样需要记录。
- 在CMS中:只要在写栅栏(write barrier)里发现要有一个白对象的引用被赋值到一个黑对象 的字段里,那就把这个白对象变成灰色的
- 在G1中,使用的是STAB(snapshot-at-the-beginning)的方式,删除的时候记录所有的对象,它有3个步骤
- 在开始标记的时候生成一个快照图,标记存活对象
- 在并发标记的时候,在写前栅栏,记录所有引用被改变了的对象,再把这些对象都变成非白的
- 这样可能会生成游离的垃圾对象,没关系,将在下次收集周期被收集
-
栅栏 Barrier:栅栏是指在原生代码片段中,当某些语句被执行时,栅栏代码也会被执行。而G1主要在赋值语句中,使用写前栅栏(Pre-Write Barrrier)和写后栅栏(Post-Write Barrrier)。
4 垃圾收集阶段
-
Collection Set: CSet
- 它记录了GC要收集的Regions集合
- 在任意一次收集暂停中,CSet所有分区都会被释放,内部存活的对象都会被转移到分配的空闲Region中。
- CSet包括需要收集的Eden Regions、Survivor Regions,而且还包括部分(1/8)Old Regions
-
G1也有类似CMS的收集动作:初始标记、并发标记、重新标记、清除、转移回收,并且也以一个串行收集器做担保机制
-
可以看出:初始标记、重新标记、清除、转移回收会造成stw
-
年轻代收集
- 年轻代收集会,不会进行并发标记,所以它全程都是STW
- 应用线程不断活动后,年轻代空间会被逐渐填满。当JVM分配对象到Eden区域失败(Eden区已满)时,便会触发一次STW式的年轻代收集
- 工作过程
- 根扫描 Root Scanning:静态和本地对象等被扫描
- 更新已记忆集合 Update RSet:对dirty卡片的分区进行扫描,来更新RSet
- RSet扫描:在收集当前CSet之前,扫描CSet分区的RSet,检测old->young这种引用情况
- 转移和回收-Object Copy:讲CSet分区存活对象的转移到新survivor或old Region,回收CSet内垃圾对象
- 引用处理:主要针对软引用、弱引用、虚引用、final引用、JNI引用;当占用时间过多时,可选择使用参数-XX:+ParallelRefProcEnabled激活多线程引用处理
-
在年轻代收集中,Eden分区存活的对象将被拷贝到Survivor分区;原有Survivor分区存活的对象,将根据对象的年龄而晋升到新的survivor分区和老年代分区。而原有的年轻代分区将被整体回收掉。
-
年老代收集
- 当堆内存占用空间超过整堆比IHOP阈值-XX:InitiatingHeapOccupancyPercent(默认45%)时,G1就会进行年老代收集
- 在年轻代收集之后或巨型对象分配之后,会去检查这个空间占比
- 年老代收集同时会执行年轻代收集,进行年老代的roots探测,既初始标记,stw
- 然后恢复应用线程,进行年老代并发标记
- stw,重新标记
- STAB处理
- 引用处理
- 继续stw,清楚垃圾
- 恢复应用线程
-
下图日志显示了老年代收集的过程
- 当堆内存占用空间超过整堆比IHOP阈值-XX:InitiatingHeapOccupancyPercent(默认45%)时,G1就会进行年老代收集
-
混合收集
- 在进行正常的年轻代垃圾收集,也会回收一部分老年代分区。会优先选取垃圾多(垃圾占用大于85%,复制算法存活对象越少效率越高)的Regions,一共1/8的年老代Regions加入Cset中
- 假设一个Region的存活对象达到95%,而进行复制,效率很低,所以G1允许浪费部分内存,那么这个Region不会被混合收集,-XX:G1HeapWastePercent:默认5%
- stw,然后将Cset中的Regions进行收集,使用复制算法
- 下一次年轻代垃圾收集进行时,在将第二个1/8的年老代Regions加入Cset中进行收集
- 当年老代内单个Region的垃圾小于等于G1HeapWastePercent时,复制大量存活对象,效率很低。此时G1会确定结束混合收集周期。所以混合收集次数可能小于8次。
- 在进行正常的年轻代垃圾收集,也会回收一部分老年代分区。会优先选取垃圾多(垃圾占用大于85%,复制算法存活对象越少效率越高)的Regions,一共1/8的年老代Regions加入Cset中
-
转移失败的担保机制 Full GC
- 当G1无法在堆空间中申请新的分区时,G1便会触发担保机制,执行一次STW式的、单线程的Full GC。Full GC会对整堆做标记清除和压缩,最后将只包含纯粹的存活对象。
- G1在以下场景中会触发Full GC,同时会在日志中记录to-space-exhausted以及Evacuation Failure
- 从年轻代分区拷贝存活对象时,无法找到可用的空闲分区
- 从老年代分区转移存活对象时,无法找到可用的空闲分区
- 分配巨型对象时在老年代无法找到足够的连续分区
- 由于G1的应用场合往往堆内存都比较大,所以Full GC的收集代价非常昂贵,应该避免Full GC的发生。
5 其他
- 避免Full GC
- -xx:+PrintAdaptiveSizePolicy 打印自适应调节策略,当出现Full GC的时候可以看到相关信息
- 避免内存空间枯竭,根据情况增加G1堆内存
- 避免过多巨型对象分配
- 避免"引用处理"过程耗时过长
- 引用处理默认是stw、单线程,可选择使用参数-XX:+ParallelRefProcEnabled激活多线程引用处理
- 找出弱引用过多的原因
- 设置合理的暂停目标,从下面例子可以看出并不是越短的暂停越好,而是需要根据实际情况取一个合理的值; 吞吐量与暂停目标对应关系,
- 50% -> 250ms:为了达到这个暂停目标,只能缩小Eden 区大小,但是程序分配内存的速率没变,所以会造成频繁的Young GC,从而影响了吞吐量
- 90% -> 300ms
- 95% -> 360ms
- 99% -> 500ms
- 99.9% -> 623ms
- 99.99% -> 748ms
- 100% -> 760ms
以上内容,是本人学习的笔记和工作中的总结,仅供大家参考,有误的地方还请指正
转载、引用请标明出处
//www.greatytc.com/p/35805f809a21
本文出自zhh_happig的简书博客,谢谢