G1垃圾收集器详解

关于G1垃圾收集器网上的文章很多,但很多都是对垃圾收集过程进行了简化,没有体现出G1的真正优势,只有掌握G1的整个垃圾处理流程才能真正明白G1的优势,由于本人水平有限,本文必然有许多错误,欢迎指正。

从CMS开始

CMS垃圾收集的流程

CMS收集器(Concurrent Mark Sweep:并发标记清除)是一种以获取最短回收停顿时间为目标的收集器。

适合在互联网站或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短(也有两个短时间的Stop The World)。CMS非常适合堆内存大、CPU核数多的服务区端应用,也是G1出现之大型应用的首选收集器。

enter description here

4步过程:

  • 初始标记(CMS initial mark)

    只是标记一下GC Roots能够直接关联的对象,速度很快,仍然需要暂停所有的工作线程。

  • 并发标记(CMS concurrent mark)

    进行GC Roots跟踪过程,和用户线程一起工作,不需要暂停工作线程。主要标记过程,标记全部对象。

  • 重新标记(CMS remark)

    为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录(例如垃圾被重新引用),仍然需要暂停所有的工作线程。

  • 并发清除(CMS concurrent sweep)

    清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程,基于标记结果,直接清理对象。

CMS的致命缺点

  1. 产生memory fragmentation(内存碎片),可能会调用FullGC进行垃圾整理,对于一个大型服务器STW可能会持续数小时之久;
  2. 在并发清除的过程中产生floating garbage(浮动垃圾);
  3. 垃圾回收操作必须扫描整个老年代,不适合超大内存;
  4. 年轻代和老年代都是独立的内存块,大小必须提前确定,无法伸缩。
  5. CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。

G1收集器介绍

-XX:+UseG1GC开启G1垃圾收集器

日志中的Heap只有garbage-first heapMataspace两部分。

G1的官网描述

https://www.oracle.com/technetwork/tutorials/tutorials-1876574.html

The Garbage-First (G1) collector is a server-style garbage collector, targeted for multi-processor machines with large memories. It meets garbage collection (GC) pause time goals with a high probability, while achieving high throughput. The G1 garbage collector is fully supported in Oracle JDK 7 update 4 and later releases. The G1 collector is designed for applications that:
   - Can operate concurrently with applications threads like the CMS collector.
   - Compact free space without lengthy GC induced pause times.
   - Need more predictable GC pause durations.
   - Do not want to sacrifice a lot of throughput performance.
   - Do not require a much larger Java heap.

G1(Garbage-First)收集器,是一款面向服务端应用的收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能的满足垃圾收集暂停时间的要求。

  • 和CMS收集器一样,能与应用程序线程并发执行
  • 整理空闲空间更快
  • 需要更多的时间来预测GC停顿时间
  • 不希望牺牲大量的吞吐性能
  • 不需要更大的Java Heap

G1收集器的设计目标是取代CMS收集器,和CMS相比,在以下方面表现更出色:

  • G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片;
  • G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间

G1收集器的特点

  1. G1能充分利用多CPU、多核环境优势,尽量缩短STW;
  2. G1整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片;
  3. 宏观上G1之中不再区分年轻代和老年代,把内存划分成多个独立的子区域(Region);
  4. G1收集器里面将整个的内存去都混合在一起了,但其本身依然在小范围内要进行年轻代和老年代的区分,保留了新生代和老年代,但它们不再是物理隔离,而是一部分Region的集合且不需要Region是连续的,也就是说依然会采用不同GC方式来处理不同的区域;
  5. G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的survivor(to space)堆做复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换。
  6. 主要改变的是Eden,Survivor和Tenured等内存区域不再是连续的了,而是变成了一个个大小相同的region(区域化),每个region从1M到32M不等。一个region有可能属于Eden,Survivor或Tenured内存区域。

G1收集器原理

Region区域化垃圾收集器,最大好处是化整为零,避免全内存扫描,只需要按照区域来进行扫描即可。

核心思想是讲整个堆内存区域分成大小相同的子区域,在JVM启动时会自动共设置这些子区域的大小。

在堆的使用上,G1并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可,每个分区也不会固定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数-XX:G1HeapRegionSize可指定分区大小(1~32M,且必须是2的幂),默认将整堆划分为2048个分区。大小范围在1-32M,最多能设置2048个区域,也即能够支持的最大内存为64G。

在G1中,还有一种特殊区域,Humongous区域,如果一个对象占据的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象默认直接会被分配在老年代,但是如果他是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,他用来专门存放巨型对象。如果一个H区装不下,那么G1就会寻找连续的H分区来存储。

为什么它叫做Garbage First:

因为所有的垃圾回收,都是基于Region的。G1根据各个Region回收所获得的空间大小以及回收所需时间等指标在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大(垃圾)的Region,从而可以有计划地避免在整个Java堆中进行全区域的垃圾收集。这也是"Garbage First" 得名的由来。

G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region之间)上看又是基于“标记-复制”算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。这种特性有利于程序长时间运行,在程序为大对象分配内存时不容易因无法找到连续内存空间而提前触发下一次GC。

G1收集器组成

RSet

为了更好的收集Region并且避免全部扫描,每个Region都会由一个RSet维护并跟踪外部对本Region所拥有的引用,当G1进行年轻代GC或者混合收集时,它会扫描包含在CSet中的分区中的RSet。在Region的对象被移动时RSet也会更新引用。简单来说,RSet里面存的是别的Region中对本Region的引用,也就是存活对象。在G1中,RSet的维护主要来源两个方面:写栅栏(Write Barrier)和并发优化线程(Concurrence Refinement Threads)。

更多参考:https://www.jianshu.com/p/870abddaba41

CSet

收集集合(CSet)代表每次GC暂停时回收的一系列目标分区。在任意一次收集暂停中,CSet所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。因此无论是年轻代收集,还是混合收集,工作的机制都是一致的。年轻代收集CSet只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到CSet中

候选老年代分区的CSet准入条件,可以通过活跃度阈值-XX:G1MixedGCLiveThresholdPercent(默认85%)进行设置,从而拦截那些回收开销巨大的对象;同时,每次混合收集可以包含候选老年代分区,可根据CSet对堆的总大小占比-XX:G1OldCSetRegionThresholdPercent(默认10%)设置数量上限。

Region中的五个指针

针对 Region 本身,需要重点理解 Region 中的五个指针

  • Bottom 指向 Region 起点;
  • Top 当前Region 分配对象的游标,Top 永远指向当前Region 最新分配的对象;
  • PrevTAMSNextTAMS 分别标记前后两次并发标记周期开始时 Top 指针的位置
  • End 表示 Region 终点。

[Bottom,PrevTAMS)-> 这部分的存活信息会在previous marking bitmap体现;

[PrevTAMS, NextTAMS)-> 这部分对象在第 n-1 轮全局标记周期是隐式存活;

[NextTAMS, Top)-> 这部分对象在第 n 轮全局标记周期是隐式存活;

SATB

G1垃圾收集器使用SATB(Snapshot At The Begin)标记存活对象

图中可以看到两个bitmap数据结构,G1 是借助 bitmap 来存放对象存活标记,每一个 bit 表示每个region中的某个对象起始地址,如果 bit 标记为 1,则表示该对象存活,bit 与对象对应有一套算法。

SATB(Snapshot-At-The-Begin)之所以叫这个名字,就是在初始标记开始时,G1 收集器打了一个快照,形成一个所谓的对象图 (Object Graph)。这个对象图记录在 next marking bitmap 之中 ,在并发标记阶段会在这个 bitmap 中 记录对象存活标记,最终Remark阶段结束后,完成对快照对象图所有标记。而NextTAMS 指针之后的内容,在这一次的GC 周期内并不关注,也不会被标记在此 bitmap 中。

进入到清理阶段,next marking bitmap 与 previous marking bitmap 会发生置换(swap),next marking bitmap 在下一次周期开始前会被清空。那么此时这个 Region 的 previous marking bitmap 可以直接表示出该Region 在 [Bottom,NextTAMS) 这个区间内存活对象数量,并且可以根据bitmap算出存活对象的具体地址**,辅助下一步的 Evacuation (选取CSet ,拷贝并合并存活对象到新的region里)**。回收的同时减少了内存碎片,当然 Evacuation 也是STW的。

参考:https://zhuanlan.zhihu.com/p/71058481

回收步骤

参考:https://blog.csdn.net/coderlius/article/details/79272773

全局并发标记

并发标记周期是G1中非常重要的阶段,这个阶段将会为混合收集周期识别垃圾最多的老年代分区。整个周期完成根标记、识别所有(可能)存活对象,并计算每个分区的活跃度,从而确定GC效率等级。

全局并发标记并非GC过程,只是标记,包含以下阶段

  • 初始标记:STW。 扫描GC Roots,标记直接可达对象并压入扫描栈(marking stack),注意此阶段会与 YGC共享STW;
  • **根分区扫描:**所有新复制到Survivor分区的对象,都需要被扫描并标记成根;
  • 并发标记:Concurrent。并发递归扫描marking stack,并标记存活对象,也会扫描SATB pre-write barrier记录的引用;
  • 最终标记:STW。对标CMS的remark阶段,但是本质不同的是,这里remark非常轻量,只需要flush SATB pre-write barrier的buffer;
  • 清理 :STW。清点和重置标记状态,但不拷贝任何对象。重置RSet,盘点活对象,如果没有活对象,就直接回收 Region 到free list。

G1 的 Minor GC

在分配一般对象时,当所有Eden Region使用达到最大阈值并且无法申请足够内存时,会触发一次YGC,借助RSet 作为根集扫描获取存活对象。每次YGC会回收所有Eden以及Survivor区,并且将存活对象复制到Old区以及另一部分的Survivor区。

注:YGC新生代的大小会由G1动态设定(5%~50%)

针对Eden区进行收集,Eden区耗尽后会被触发,小区域收集+形成连续的内存块,避免内存碎片。

  • Eden区的数据移动到Survivor区,假如出现Survivor区空间不够,Eden区数据就会晋升到Old区;
  • Survivor区的数据移动到新的Survivor区,部分数据晋升到Old区;
  • 最后Eden区收拾干净了,GC结束,用户的应用程序继续执行。

下面是一段经过抽取的GC日志

GC pause (G1 Evacuation Pause) (young)  ├── Parallel Time    ├── GC Worker Start    ├── Ext Root Scanning    ├── Update RS    ├── Scan RS    ├── Code Root Scanning    ├── Object Copy  ├── Code Root Fixup  ├── Code Root Purge  ├── Clear CT  ├── Other    ├── Choose CSet    ├── Ref Proc    ├── Ref Enq    ├── Redirty Cards    ├── Humongous Register    ├── Humongous Reclaim    ├── Free CSet

由这段GC日志我们可知,整个YGC由多个子任务以及嵌套子任务组成,且一些核心任务为:Root Scanning,Update/Scan RS,Object Copy,CleanCT,Choose CSet,Ref Proc,Humongous Reclaim,Free CSet。

G1 的 Mixed GC

当越来越多的对象晋升到老年代Old Region 时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即Mixed GC,是收集整个新生代以及部分老年代的垃圾收集。除了回收整个Young Region,还会回收一部分的Old Region ,这里需要注意:是一部分老年代,而不是全部老年代,可以选择那些回收收益最高Old Region进行收集,从而可以对垃圾回收的耗时时间进行控制。

三色标记算法

并发标记的难点在于:在标记对象的过程中,对象引用关系正在发生改变。

将对象逻辑上分为三种颜色:

  • 白色:未被标记的对象
  • 灰色:自身被标记,成员变量未被标记
  • 黑色:自身和成员变量均已标记完成

只有下面一种变化会导致错误垃圾标记:

打破上述两个条件之一:

  1. increamental update(增量更新),关注引用的增加,把黑色重新标记为灰色,下次重新扫描属性,CMS使用该方法
  2. SATB snapshot at the beginning,关注引用的删除,当B->D消失时,要把这个引用推到GC的堆栈,保证D还能被GC扫描到,G1使用该方法。G1采用的是pre-write barrier解决这个问题。简单说就是在并发标记阶段,当引用关系发生变化的时候,通过pre-write barrier函数会把这种这种变化记录并保存在一个队列里,在JVM源码中这个队列叫satb_mark_queue。在remark阶段会扫描这个队列,通过这种方式,旧的引用所指向的对象就会被标记上,其子孙也会被递归标记上,这样就不会漏标记任何对象,snapshot的完整性也就得到了保证。

配置参数

  • -XX:+UseG1Gc

  • -XX:G1HeapRegionSize=n

    设置的G1区域的大小,值是2的幂,范围是1-32MB,目标是根据最小的java堆大小划分出约2048个区域

  • -XX:MaxGCPauseMillis=n

    最大GC停顿时间,这是个软目标,JVM将尽可能(但不保证)停顿小于这个时间(100)

  • -XX:InitiatingHeapOccupancyRercent=n

    堆占用了多少的时候就触发GC,默认45

  • -XX:ConcGcThreads=n

    并发GC使用的线程数

  • -XX:G1ReservePercent=n

    设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险,默认10%

发布了55 篇原创文章 · 获赞 45 · 访问量 3万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览