Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。早在JDK 7刚刚确立项目目标、Oracle公司制定的JDK 7 RoadMap里面,G1收集器就被视作JDK 7中HotSpot虚拟机的一项重要进化特征。从JDK 6 Update 14开始就有Early Access版本的G1收集器供开发人员实验和试用,但由此开始G1收集器的“实验状态”(Experimental)持续了数年时间,直至JDK 7 Update 4,Oracle才认为它达到足够成熟的商用程度,移除了“Experimental”的标识;到了JDK 8 Update 40的时候,G1提供并发的类卸载的支持,补全了其计划功能的最后一块拼图。这个版本以后的G1收集器才被Oracle官方称为“全功能的垃圾收集器”(Fully-Featured Garbage Collector)。
设计目标
堆被划分为一组大小相等的堆区域,每个区域都有一个连续的虚拟内存范围。G1在标记阶段是并发执行的,当并发标记结束后,G1会知道哪些区域大部分为空,会首先收集这些区域,这样就会产生大量的空闲空间。
这就是G1收集器被称为Garbage-first的原因。
G1使用暂停预测模型来降低停顿,并根据指定的暂停目标来选择回收的区域数。
G1会将对象从堆中多个区域中复制到一个区域,在这个过程中会对内存进行压缩和释放,这个过程也是并发执行的,降低了停顿时间,增加了吞吐量。CMS(Conrrent Mark Sweep)虽然也是并发收集,但是会造成空间碎片。
G1收集器并不是实时(real-time)的,这就意味着虽然能达到短停顿,但是并不是绝对的。
对用户设定的gc停顿时间并不是决定不超过的。G1会动态评估被回收的区域可以满足用户的设定:
- 并发执行,低停顿
- 会对碎片内存进行整理
- gc停顿更加可控
- 不牺牲系统的吞吐量
- GC不要求额外的内存空间(CMS需要预留空间存储浮动垃圾)
内存划分
除G1收集器外,都会对堆内存进行分代划分,然后通过分代收集算法进行垃圾收集。
G1对堆的划分做了特殊处理,将整个堆内存进行了固定大小的划分。并且这些区域可以是不连续的。
划分出来的区域会有接近2000个,每个区域的大小从1mb~32mb不等。
由于G1对堆内存的结构重新进行了划分,所以就不会有固定大小的eden区,Survivor区的概念,这些区域都能动态调整。
Humongous区域
除了这三种类型,还会有一种大型区域(Humongous regions)的类型这种区域是设计用于存放超过标准区域50%大小的对象的。这些对象会存放在连续的区域中。
默认情况下,这种超大的对象会直接分配在老年代,但是如果这种超大对象是是短期存在的,就会对垃圾收集器造成负面影响,为了解决这个问题,G1专门为这种对象划分了一个Humongous区,用于存放这些对象.如果一个Region还放不下,就会找到内存连续的下一个Region来存储,如果找不到连续的Region区域,就会触发FullGC。
G1堆结构
- heap被划分为一个个相等的不连续区域
- 每个区域中的数量没有强制限定,可以动态变化
- 优先回收有大量可回收对象的区域
G1重要概念
每个分区都可能是年轻代或者老年代,但是在同一时刻只能属于某个代.年轻代、Survivor区、老年代这些概念还存在,但是已经成为逻辑上的概念,这样方便复用之前分代框架的逻辑。
在物理上不需要连续,则带来了额外的好处–有的分区内垃圾对象特别多,有的分区内垃圾,G1会优先回收垃圾对象特别多的分区。当新生代满了时候,依然会对整个新生代进行回收,整个新生代的对象要么被回收,要么晋升,至于新生代也采取分区机制的原因,是因为跟老年代策略统一,方便调整整个代的大小。
G1有压缩收集器,在回收老年代的分区时,会将存活对象从一个分区拷贝到另一个可用分区,拷贝过程实现了局部压缩。
如果从ParallelOldGC或者CMS迁移到G1收集器,会发现jvm占用更大的进程空间,原因是用于存储计算数据的结构。例如RSet(Remembered Set)和CSet(Collection Set)。
记忆集合(Remembered Sets)
每个region都会有一个RSet,RSet记录了不同区域之间对象引用的关系,RSet的价值在于使得垃圾收集器不需要扫描整个堆找到分区中对象的引用,只需扫描RSet即可。
收集集合(Collection Set)
每个region都会有一个CSet,是一组可被回收的分区的集合。在CSet中存活的数据会在GC过程中被移动到另一个可用分区,CSet中的分区可以来自eden区,Survivor区,老年代。
G1使用场景
G1主要目标是解决应用程序在限定的GC延迟内需要很高的堆内存的情况。如果正在使用ParallelOldGC或者CMS的应用切换到G1后会享受到更大的优势。
使用场景
- FullGC时间太长或太频繁
- 对象分配或对象晋升的速率变化明显
- 不希望GC停顿太长(超过0.5-1s)
如果应用正在使用ParallelGC或者CMS,但是并没有感受到明显的停顿,继续保留也可以。因为使用G1需要更新的JDK版本。
G1 GC模式
- G1提供了两种GC模式,Young GC和Mixed GC,两种都是STW。
- Young GC:选定所有年轻代中的Region,通过控制年轻代Region个数,即年轻代内存大小来控制Young GC的时间。
- Mixed GC:选定所有年轻代里的Region,外加根据global concurrent marking统计所得出收集效益高的老年代Region。在用户指定的开销目标(指定GC停顿时间)内尽可能选择收益高的老年代Region。
Mixed GC不是Full GC,只能回收部分老年代Region,如果Mixed GC无法跟上程序分配内存的速度,会导致老年代填满而无法继续进行Mixed GC,就会使用SerialOldGC来收集整个heap,本质上G1不提供Full GC。
触发MixedGC的场景
因为Mixed GC是同时对年轻代和老年代进行回收。所以一般情况下,Mixed GC会被部分参数控制,同时这些参数也控制着哪些老年代Region会进入CSet。
- G1HeapWastePercent:在并发标记结束后,可以知道老年代区域中有多少空间要被回收,在每次Young GC之后和再次发生Mixed GC之前,会检查垃圾占比是否达到此参数,达到后就会发生Mixed GC。
- G1MixedGCLiveThreasholdPercent:老年代region中的存活对象比例,只有在此比例之下,才会被选入CSet。
例如设置30%,那就是存活对象占比低于region大小30%时会被选入CSet。
- G1MixedGCCountTarget:一次并发标记结束后,最多执行MixedGC的次数。
- G1OldCSetRegionThreadholdPercent:一次Mixed GC中能被选入CSet的老年代的比例。
年轻代的GC
年轻代的对象会被复制到一个或多个Survivor区,如果到达了年龄阈值或者Survivor空间不足,则会晋升到老年代区域,最终Eden区域的数据为空。
总结
- 堆内存被划分为相同大小的区域region
- 年轻代是由一组不连续的region组成,可以在需要的时候对大小进行调整
- 年轻代的GC也是STW的,所有的用户线程会停顿
- 年轻代的GC是多线程并发执行的
- 存活对象通过复制算法复制到多个Survivor区域或者老年代
老年代的GC
与CMS收集器类似,G1在老年代中的GC也是被设计为低停顿的。
老年代的GC过程
其中某些阶段也是年轻代gc阶段。
1.初始化标记(Initial Mark),STW,标记从GCRoot开始直接可达的对象
2.根区域扫描(Root Region Scanning)。扫描Survivor区与老年代中对象的引用。这个阶段与用户线程并行执行,这个阶段会在年轻代gc前结束。
3.并发标记(Concurrent Marking)。找到整个heap的存活对象,与用户线程并发执行,这个阶段可能会被年轻代的gc中断。
4.标记(Remark)。使用SATB算法完成所有的存活对象标记。这个算法比CMS中的使用更快。
5.清理(Cleanup)。清理所有的区域(SWT),清理RSet(STW),重置空的区域,将区域归还到空闲列表。
6.复制(Copying)。将存活对象复制到空闲区域。如果是年轻代发生,在GC日志中会被记录为[GC pause(young)],如果是年轻代和老年轻一起复制,会被记录为[GC Pause (mixed)]。
初始化标记(Initial Marking)
初始化标记是标记在年轻代的存活对象,这个阶段是STW的。
并发标记阶段(Concurrent Marking)
在这个阶段,如果有空闲区域被发现,在并发标记阶段会直接被清除。此外,统计信息accounting会在这个阶段进行(RSet,CSet)重
重新标记阶段(Remark)
STW,空闲区域会被移除和回收,区域中的存活对象会被标记出来。
复制,清理阶段(Copying Cleanup Phase)
STW,G1会选择“最不活跃”的区域,这些区域通常都能被很快地回收。在年轻代GC这些区域会被并发地进行回收。
复制或清理结束后对象的分布。
老年代GC总结
- 并发标记阶段
- 活跃信息会并发计算出来
- 找到最合适的清理区域
- 这个阶段不进行清理
- 重新标记阶段
- 使用SATB(Snapshot-at-the-Begining)算法
- 完成对空闲区域的回收
- 复制,清理阶段
- 年轻代和老年代会同时回收
- 老年代的回收会基于对象存活信息
G1重要参数
参数 | 描述 |
-XX:+UseG1GC | 使用G1收集器 |
-XX:MaxGCPauseMillis=n | 最大的gc停顿时间,毫秒为单位,默认是200ms.这个参数不是绝对的,jvm会尽量达到这个值. |
-XX:InitiatingHeapOccupancyPercent=n | 设置触发标记周期的堆(包含所有区域)占用率阈值。默认值是45%。 |
-XX:ParallelGCThreads=n | 并行执行阶段线程数(STW阶段) |
-XX:ConcGCThreads=n | 并发阶段的线程数(非STW阶段) |
Region
每一个Region 包含了5个指针,分别是bottom、previous TAMS、next TAMS、top和end, 其中previous TAMS、next TAMS是前后两次发生并发标记时的位置。在prevTAMS和nextTAMS以上的对象就是新分配的。
- A是初始标记阶段。next TAMS尚未标记任何存活对象,而此时的previous TAMS被初始化为region内存地址起始值,next TAMS被初始化为top。top实际上就是一个region未分配区域和已分配区域的分界点;
- B是并发标记之后,进入了再次标记阶段。此时存活对象的扫描已经完成了,因此next bitmap构造好了,代表的是当下状态中region中的内存使用情况。注意的是,此时top已经不再与next TAMS重合了,top和next TAMS之间的就是在前面标记阶段之时,新分配的对象
- C代表的是clean up阶段。C和B比起来,next bitmap变成了previous bitmap,而在bitmap中标记为垃圾(也就是白色区域的)的对应的region的区域也被染成了浅灰色。这并不是指垃圾对象已经被清扫了,仅仅是标记出来了。
- D代表的是下一个初始标记阶段,该阶段和A类似,next TAMS重新被初始化为top的值;
- EF就是BC的重复;
总结
G1的特点可以简单总结如下:
- 并发收集;
- 压缩空闲空间不会延长GC的暂停时间;
- 更容易预测的GC暂停时间;
- 更适用于响应时间优先的场景;