垃圾收集器与内存分配策略(22)


G1收集器
First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于的内存布局形式 。早在JDK 7刚刚确立项目目标、公司制定的JDK 7 里面,G1收集器就被视作JDK 7中虚拟机的一项重要进化特征 。从JDK 614开始就有Early 版本的G1收集器供开发人员实验和试用,但由此开始G1收集器的“实验状态”()持续了数年时间,直至JDK 74,才认为它达到足够成熟的商用程度,移除了“”的标识;到了JDK 840的时候,G1提供并发的类卸载的支持,补全了其计划功能的最后一块拼图 。这个版本以后的G1收集器才被官方称为“全功能的垃圾收集器”(Fully-) 。
G1是一款主要面向服务端应用的垃圾收集器 。开发团队最初赋予它的期望是(在比较长期的)未来可以替换掉JDK 5中发布的CMS收集器 。现在这个期望目标已经实现过半了,JDK 9发布之日,G1宣告取代 加 Old组合,成为服务端模式下的默认垃圾收集器,而CMS则沦落至被声明为不推荐使用()的收集器 。
在G1收集器出现之前的所有其他收集器,包括CMS在内,垃圾收集的目标范围要么是整个新生代(Minor GC),要么就是整个老年代(Major GC),再要么就是整个Java堆(Full GC) 。而G1跳出了这个樊笼,它可以面向堆内存任何部分来组成回收集( Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式 。
G1开创的基于的堆内存布局是它能够实现这个目标的关键 。虽然G1也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(),每一个都可以根据需要,扮演新生代的Eden空间、空间,或者老年代空间 。收集器能够对扮演不同角色的采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果 。
中还有一类特殊的区域,专门用来存储大对象 。G1认为只要大小超过了一个容量一半的对象即可判定为大对象 。每个的大小可以通过参数-XX:设定,取值范围为1MB~32MB,且应为2的N次幂 。而对于那些超过了整个容量的超级大对象,将会被存放在N个连续的 之中,G1的大多数行为都把 作为老年代的一部分来进行看待
虽然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列区域(不需要连续)的动态集合 。G1收集器之所以能建立可预测的停顿时间模型,是因为它将作为单次回收的最小单元,即每次收集到的内存空间都是大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集 。更具体的处理思路是让G1收集器去跟踪各个里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:指定,默认值是200毫秒),优先处理回收价值收益最大的那些,这也就是“ First”名字的由来 。这种使用划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率 。
将Java堆分成多个独立后,里面存在的跨引用对象如何解决?
解决的思路我们已经知道:使用记忆集避免全堆作为GC Roots扫描,但在G1收集器上记忆集的应用其实要复杂很多,它的每个都维护有自己的记忆集,这些记忆集会记录下别的指向自己的指针,并标记这些指针分别在哪些卡页的范围之内 。G1的记忆集在存储结构的本质上是一种哈希表,Key是别的的起始地址,Value是一个集合,里面存储的元素是卡表的索引号 。这种“双向”的卡表结构(卡表是“我指向谁”,这种结构还记录了“谁指向我”)比原来的卡表实现起来更复杂,同时由于数量比传统收集器的分代数量明显要多得多,因此G1收集器要比其他的传统垃圾收集器有着更高的内存占用负担 。根据经验,G1至少要耗费大约相当于Java堆容量10%至20%的额外内存来维持收集器工作 。