JVM 垃圾回收器之---Garbage First(G1)

Garbage First(G1)
设计思想
随着 JVM 中内存的增大, STW 的时间成为 JVM 急迫解决的问题, 但是如果按照传统的分代模型, 总跳不出 STW 时间不可预测这点。
为了实现 STW 的时间可预测, 首先要有一个思想上的改变。 G1 将堆内存“化整为零” , 将堆内存划分成多个大小相等独立区域(Region) , 每一个 Region
都可以根据需要, 扮演新生代的 Eden 空间、 Survivor 空间, 或者老年代空间。 回收器能够对扮演不同角色的 Region 采用不同的策略去处理, 这样无论是
新创建的对象还是已经存活了一段时间、 熬过多次收集的旧对象都能获取很好的收集效果。
Region
Region 可能是 Eden,也有可能是 Survivor,也有可能是 Old,另外 Region 中还有一类特殊的 Humongous 区域, 专门用来存储大对象。 G1 认为只要大小超过
了一个 Region 容量一半的对象即可判定为大对象。 每个 Region 的大小可以通过参数-XX:G1HeapRegionSize 设定, 取值范围为 1MB~32MB,且应为 2 的 N 次
幂。 而对于那些超过了整个 Region 容量的超级大对象, 将会被存放在 N 个连续的 Humongous Region 之中, G1 的进行回收大多数情况下都把 Humongous
Region 作为老年代的一部分来进行看待。

参数设置
开启参数
-XX:+UseG1GC
分区大小
-XX:+G1HeapRegionSize

一般建议逐渐增大该值, 随着 size 增加, 垃圾的存活时间更长, GC 间隔更长, 但每次 GC 的时间也会更长。
最大 GC 暂停时间
       MaxGCPauseMillis

运行过程

G1 的运作过程大致可划分为以下四个步骤:
初始标记( Initial Marking)
仅仅只是标记一下 GC Roots 能直接关联到的对象, 并且修改 TAMS 指针的值, 让下一阶段用户线程并发运行时, 能正确地在可用的 Region 中分配新对象。
这个阶段需要停顿线程, 但耗时很短, 而且是借用进行 Minor GC 的时候同步完成的, 所以 G1 收集器在这个阶段实际并没有额外的停顿。
TAMS 是什么?
要达到 GC 与用户线程并发运行, 必须要解决回收过程中新对象的分配, 所以 G1 为每一个 Region 区域设计了两个名为 TAMS(Top at Mark Start) 的指针,
从 Region 区域划出一部分空间用于记录并发回收过程中的新对象。 这样的对象认为它们是存活的, 不纳入垃圾回收范围。
并发标记( Concurrent Marking)
从 GC Root 开始对堆中对象进行可达性分析, 递归扫描整个堆里的对象图, 找出要回收的对象, 这阶段耗时较长, 但可与用户程序并发执行。 当对象图扫
描 完 成 以 后 , 并 发 时 有 引 用 变 动 的 对 象 , 这 些 对 象 会 漏 标 ( 三 色 标 记 会解决 个 问 题 ) , 漏 标 的 对 象 会 被 一 个 叫 做
SATB(snapshot-at-the-beginning)算法来解决
最终标记( Final Marking)
对用户线程做另一个短暂的暂停, 用于处理并发阶段结后仍遗留下来的最后那少量的 SATB 记录(漏标对象)。
筛选回收( Live Data Counting and Evacuation)
负责更新 Region 的统计数据, 对各个 Region 的回收价值和成本进行排序, 根据用户所期望的停顿时间来制定回收计划, 可以自由选择任意多个 Region 构
成回收集, 然后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中, 再清理掉整个旧 Region 的全部空间。 这里的操作涉及存活对象的移动,
是必须暂停用户线程, 由多条收集器线程并行完成的。
特点
     并行与并发: G1 能充分利用多 CPU、 多核环境下的硬件优势, 使用多个 CPU(CPU 或者 CPU 核心) 来缩短 Stop-The-World 停顿的时间, 部分其他收集器
原本需要停顿 Java 线程执行的 GC 动作, G1 收集器仍然可以通过并发的方式让 Java 程序继续执行。
     分代收集: 与其他收集器一样, 分代概念在 G1 中依然得以保留。 虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆, 但它能够采用不同的方式
去处理新创建的对象和已经存活了一段时间、 熬过多次 GC 的旧对象以获取更好的收集效果。
    空间整合: 与 CMS 的“标记—清理” 算法不同, G1 从整体来看是基于“标记—整理” 算法实现的收集器, 从局部(两个 Region 之间) 上来看是基于“复
制” 算法实现的, 但无论如何, 这两种算法都意味着 G1 运作期间不会产生内存空间碎片, 收集后能提供规整的可用内存。 这种特性有利于程序长时间运
行, 分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC。
    追求停顿时间:
-XX:MaxGCPauseMillis 指定目标的最大停顿时间, G1 尝试调整新生代和老年代的比例, 堆大小, 晋升年龄来达到这个目标时间。
怎么玩?
该垃圾回收器适合回收堆空间上百 G。 一般在 G1 和 CMS 中间选择的话平衡点在 6~8G, 只有内存比较大 G1 才能发挥优势。

G1 中的技术细节
   

跨代引用
堆空间通常被划分为新生代和老年代。 由于新生代的垃圾收集通常很频繁, 如果老年代对象引用了新生代的对象, 那么回收新生代的话, 需要跟踪从老
年代到新生代的所有引用, 所以要避免每次 YGC 时扫描整个老年代, 减少开销。
RSet(记忆集)
记录了其他 Region 中的对象到本 Region 的引用,
RSet 的价值在于使得垃圾收集器不需要扫描整个堆,找到谁引用了当前分区中的对象, 只需要扫描 RSet 即可。
RSet 本身就是一个 Hash 表, 如果是在 G1 的话, 则是在一个 Region 区里面。
CardTable
由于做新生代 GC 时, 需要扫描整个 OLD 区, 效率非常低, 所以 JVM 设计了 CardTable,如果一个 OLD 区 CardTable 中有对象指向 Y 区, 就将它设为 Dirty
(标志位 1) , 下次扫描时, 只需要扫描 CARDTABLE 上是 Dirty 的内存区域即可。
字节数组 CARDTABLE 的每一个元素都对应着其标识的内存区域中一块特定大小的内存块, 这个内存块被称作“卡页” (Card Page)。 一般来说, 卡页大小
都是以 2 的 N 次幂的字节数, 假设使用的卡页是 2 的 10 次幂, 即 1K,内存区域的起始地址是 0x0000 的话, 数组 CARD_TABLE 的第 0、 1、 2 号元素, 分别
对应了地址范围为 0x0000~0x03FF、 0x0400 ~ 0x07FF、 0x0800~0x011FF 的卡页内存.
总结
这里描述的是 G1 处理跨代引用的细节, 其实在 CMS 中也有类似的处理方式, 比如 CardTable,也需要记录一个 RSet 来记录, 我们对比一下, 在 G1 中是每
一个 Region 都需要一个 RSet 的内存区域, 导致有 G1 的 RSet 可能会占据整个堆容量的 20%乃至更多。 但是 CMS 只需要一份, 所以就内存占用来说, G1
占用的内存需求更大, 虽然 G1 的优点很多, 但是我们不推荐在堆空间比较小的情况下使用 G1, 尤其小于 6 个 G。
 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值