G1全称是GarbageFirstGarbageCollector,使用G1的目的是简化性能优化的复杂性。例如,G1的主要输入参数是初始化和最大Java堆大小、最大GC中断时间。
G1GC由YoungGeneration和OldGeneration组成。G1将Java堆空间分割成了若干个Region,即年轻代/老年代是一系列Region的集合,这就意味着在分配空间时不需要一个连续的内存区间,即不需要在JVM启动时决定哪些Region属于老年代,哪些属于年轻代。因为随着时间推移,年轻代Region被回收后,又会变为可用状态(后面会说到的UnusedRegion或AvailableRegion)了。
G1年轻代收集器是并行Stop-the-world收集器,和其他的HotSpotGC一样,当一个年轻代GC发生时,整个年轻代被回收。G1的老年代收集器有所不同,它在老年代不需要整个老年代回收,只有一部分Region被调用。
G1GC的年轻代由EdenRegion和SurvivorRegion组成。当一个JVM分配EdenRegion失败后就触发一个年轻代回收,这意味着Eden区间满了。然后GC开始释放空间,第一个年轻代收集器会移动所有的存储对象从EdenRegion到SurvivorRegion,这就是“CopytoSurvivor”过程。
清单1所示是年轻代的回收GC输出日志,在这个日志里面,请见最后一行,年轻代新的大小是(NewEden)+32(NewSurvivor)=MB
清单1G1回收年轻代
当一个Java堆空间瓶颈点超过后,即堆空间耗尽,这时G1初始化老年代收集器,这个Initial-Mark阶段是一个并行Stop-the-World的,它的大小和老年代以及整个Java堆大小有关。
Initial-Mark阶段和下一个YoungGC一起执行,一旦Initial-Mark完成,一个多线程并行的Marking阶段开始去标记老年代所有存活的对象。当并行Marking阶段完成,一个并行的Stop-the-World的阶段开始去标记所有的对象(由于和Marking阶段并行存在,应用程序多线程程序可能会丢失数据)。Remark节点结束后,G1有老年代所有Region的完整的标记信息,如果这时老年代没有任务存活对象,那么下一个阶段Cleanup阶段就不需要额外的GC工作了。
上面的描述是关于老年代收集器的流程描述,简要说明就是Initial-Mark-ConcurrentRootRegionscanning-ConcurrentMarking-Remarking-Cleanup。
在Cleanup阶段,如果G1GC发现一个可回收的Region,不用等到下一个GC停顿可以直接回收这些Region并且加入到空闲Region的LinkedList链表。
CMS、Parallel、SerialGC都需要通过FullGC去压缩老年代并在这个过程中扫描整个老年代。
因为G1的操作以Region为基础,因此它适用于大Java堆。即便Java堆很大,大量的GC工作可以被限制在小型Region集合里面。G1允许用户指定停顿时间目标,G1通过自适应的堆大小来满足这个目标。
※G1GC深度原理G1把整个Java堆划分为若干个区间(Regions)。每个Region大小为2的倍数,范围在1MB-32MB之间,可能为1,2,4,8,16,32MB。所有的Region有一样的大小,JVM生命周期内不会改变。例如-Xmx16g–Xms16g,设置16GB的堆大小,个Regions,则每个Region=16GB/=8MB。如果堆大小很大,而每个Region的大小很小,则Region数量可能会超过个。同样地,很小的堆大小会导致Region数量很少。
Region类型
G1对于Region的几个定义:
AvailableRegion=可用的空闲Region
EdenRegion=年轻代Eden空间
SuivivorRegion=年轻代Survivor空间
所有Eden和Survivor的集合=整个年轻代
HumongousRegion=大对象Region
HumongousRegion
大对象是指占用大小超过一个Region50%空间的对象,这个大小包含了Java对象头。对象头大小在32位和64位HotSpotVM之间有差异,可以使用JavaObjectLayout工具确定头大小,简称JOL。
当大对象开始进入排队时,G1会调动几个连续的有效Region存放它。第一个Region叫做“大对象开始”Region,其他Regions叫“大对象延续”Regions。如果没有连续的可用Regions,G1会做一个Javaheap的fullgc去压缩对象。大对象区间属于老年代的一部分,它只包含一个对象,这个属性允许G1收集一个大对象区间当并行Marking阶段发现没有对象存活时。当这个条件触发,所有包含大对象的区间都可以立即被回收申明。
对于G1来说,一个潜在的挑战是短生命周期的大对象可能不会被申明直到它们变成没有被引用。JDK8U45申明了一个方法在年轻代回收大对象Region。
前面说过,G1Region包括YoungRegion、OldRegion、HumougousRegion、FreeRegion。每个收集器单元跨越一个年轻和老年Regions。一个大对象跨越两个收集器单元,所以大对象Region是一个连续的Region,如图1所示。
图1Region跨越分布图1
我们再来看图2,大对象1跨越两个连续区间。第一个连续Region被标记为StartHumongous,接下来连续的Region叫做ContinuesHumongous。大对象2跨越三个连续的堆Regions,大对象3跨越了一个Region。
图2Region跨越分布图2
RSet
基于老年代的收集器采用Heap里不同区域区分/隔离对象,这些不同的区域里面的对象对应了不同年代。这样年代收集器可以集中精力在最近分配的对象上,因为它们会发现一些对象不久会死亡。这些年代在堆里可以被分别收集,这样不用扫描整个Heap,可以节省时间和减小响应时间,并且存活时间长的对象不用来回复制,减少了拷贝和引用更新开销。
为了方便收集器的独立性,许多GC维持了每个年代的RSet。每一个RSet是一个数据结构,它维护并跟踪收集器单元的内部引用,如G1GC的Region一样,减少了扫描整个Heap堆获取信息的耗时。当G1GC执行了一个Stop-the-world收集(年轻代或混合代),它可以通过CSet扫描Region的RSets。一旦存活对象被移除,它们的引用立即被更新。
图3RSet布局图示例
从上面的图3,我们可以看见一个年轻代Region(RegionX)和两个老年代Region(RegionY和RegionZ)。RegionX有一个从RegionZ来的引用。这个引用被标记在了RegionX的RSet里面。我们也观察到RegionZ有两个引用,一个来自于RegionX,另一个来自于RegionY。RegionZ的RSet需要标记从RegionY过来的引用,但是不需要去记住从RegionX来的引用,因为年轻代是全局被收集的。对于RegionY,最终我们可以看到从RegionX来的引用,并没有在RegionY的RSet里记录引用。
MixedGC事件
随着很多对象被提升到老年代,以及大对象进入大对象区间,整个Java堆区占有率上升。为了避免Java堆空间溢出,JVM进程需要去初始化一个GC(不仅包含年轻代Regions,也包含增加老年代Region到混合收集器)。在混合GC事件里,所有的年轻代Regions会被收集,同时一部分老年代Region也会被收集。
清单2G1老年代回
FullGarbageCollections
G1内部,前面提到的混合GC是非常重要的释放内存机制,它避免了G1出现Region没有可用的情况,否则就会触发FullGC事件。
CMS、Parallel、SerialGC都需要通过FullGC去压缩老年代并在这个过程中扫描整个老年代。G1的FullGC算法和SerialGC收集器完全一致。当一个FullGC发生时,整个Java堆执行一个完整的压缩,这样确保了最大的空余内存可用。G1的FullGC是一个单线程,它可能引起一个长时间的停顿时间,G1的设计目标是减少FullGC,满足应用性能目标。
※G1GC常用参数我在这里所罗列的参数的默认值都是基于JDK8u45,所以可能后续的JDK版本会有些值不一样,这个读者可以直接通过JDK的官方帮助文档获取最新默认值信息。
-XX:+UseG1GC:启用G1GC。JDK7和JDK8要求必须显示申请启动G1GC,JDK可能会设置G1GC为默认GC选项,也有可能会退到早期的ParallelGC,这个也请