深入理解Java虚拟机——G1垃圾收集器

163 阅读3分钟

G1开创“面向局部收集的设计思路”和“基于Region的堆内存布局形式”。

基于Region的堆内存布局

把连续的Java堆划分为多个大小相等的独立区域,Region根据需要扮演新生代或者老年代,收集器根据不同角色的Region采用不同处理策略。

Humongous区域

Humongous区域用于存储大对象,G1认为只要大小超过一个Region容量一半的对象即可判定为大对象,每个Region的大小通过参数-XX: G1HeapRegionSize设定,范围为1MB~32MB。对于超过整个Region的超级大对象,会存放在连续的Humongous中,对于Humongous区域,垃圾收集的行为与对待老年代相同。

G1解决的问题

跨代引用

由于垃圾收集是以Region为基本单位的,因此必须知道哪些其他Region中有对该Region的引用。对此G1维护了一个Map,Key为其他Region的起始地址,Value是卡表索引号的集合。通过Key可以判断有哪些Region引用了我,通过Value可以查出具体引用了哪些对象。

并发标记阶段用户线程和收集线程不干扰

G1通过原始快照的方式来解决,即当出现“删除全部灰色对象到白色对象的引用”时,将删除的引用关系记录下来,并发扫描后再进行一次搜索。

新创建对象的内存分配

Region中有两个TAMS指针,把Region中的一块内存划分出来用于并发回收过程中的新对象分配,所有新对象都分配到这块内存中,默认为存过,不纳入回收范围。如果内存回收速度赶不上内存分配速度,G1也要冻结用户线程的执行,导致Full GC而长时间STW。

G1的垃圾收集可以分为四个部分

  1. 初始标记:(单线程,STW)标记出GC Roots,并修改TAMS指针的值,为了用户线程运行时有可用的Region来分配新对象。这个阶段需要停顿但耗时很短。并且在Minor GC的时候同步完成,实际没有额外的停顿。
  2. 并发标记:(多线程)根据GC Root开始对堆中的对象进行可达性分析,与用户线程并行。
  3. 最终标记:(多线程,STW)暂停用户线程,处理并发阶段结束后遗留下的STAB记录。
  4. 筛选回收:(多线程,STW)统计Region的数据,根据回收价值和成本排序,根据用户期望的停顿时间来指定回收计划。实际回收时会将存活的对象复制到空Region中,清理掉旧Region的全部空间。由于需要移动存活对象,必须暂停用户线程。

image.png

G1根据用户期望的停顿时间来制定垃圾收集计划,默认停顿时间是200ms,如果指定的期望停顿时间过低,垃圾收集速度跟不上分配速度,导致垃圾慢慢堆积,最终占满堆引发Full GC而降低性能,通常把期望停顿时间设置为一两百毫秒三四百毫秒比较合理。

从G1垃圾收集器开始,垃圾收集器不再追求把整个Java堆全部清理干净,而是追求能够应付应用的内存分配速率,G1是收集器技术发展从追求“停顿最短的整体清理”,向“满足应用内存分配速率、以预测性为目标的区域化收集”的一个里程碑