简介
-
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
-
Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大差别,并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。
-
比如,HotSpot虚拟机的垃圾收集器如下 :
Serial收集器
介绍
* 是最基本、发展历史最悠久的收集器
* 在JDK 1.3.1之前是虚拟机新生代收集的唯一选择
* 是虚拟机运行在Client模式下的默认新生代收集器
* 是一个单线程的收集器
* 优点:简单而高效(与其他收集器的单线程比)。
* 缺点:在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
运行示意图
ParNew收集器
介绍
* 是Serial收集器的多线程版本
* 使用多条线程进行垃圾收集
* 许多运行在Server模式下的虚拟机中首选的新生代收集器
* 除了Serial收集器外,目前只有它能与CMS收集器配合工作
运行示意图
Parallel Scavenge收集器
介绍
* 是一个新生代收集器
* 是一个多线程收集器
* 是一个吞吐量优先收集器【吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间】
* 使用的复制算法
* 主要适合在后台运算而不需要太多交互的任务
参数
* -XX:MaxGCPauseMillis
最大垃圾收集停顿时间(ms)
* -XX:GCTimeRatio
是垃圾收集时间占总时间的比率,值是一个大于0且小于100的整数。
比如参数设置为19,那允许的最大GC时间就占总时间的百分之五 (1/(1+19))。
默认值为99,就是允许最大百分之一(即1/(1+99))的垃圾收集时间。
* -XX:+UseAdaptiveSizePolicy
是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、
Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:
PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性
能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种
调节方式称为GC自适应的调节策略(GC Ergonomics)。
Serial Old收集器
介绍
* 是Serial收集器的老年代版本
* 是一个单线程收集器
* 使用标记-整理算法
* 主要在Client模式下的虚拟机中使用
* 如果在Server模式下,那么它主要有两大用途:
1. 在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用
2. 作为CMS收集器的后备预案,在并发收集发生ConcurrentMode Failure时使用
运行示意图
Parallel Old收集器
介绍
* 是Parallel Scavenge收集器的老年代版本
* 是一个多线程收集器
* 使用标记-整理算法
* JDK 1.6中提供的收集器
* 在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器
运行示意图
CMS收集器
介绍
* CMS(Concurrent Mark Sweep)
* 是一个老年代收集器
* 是一个以获取最短回收停顿时间为目标的收集器
* 使用标记-清除算法
* 缺点:
1.对CPU资源非常敏感
2.无法处理浮动垃圾
3.基于标记—清除算法,会产生空间碎片
运行示意图
运行过程:
1.初始标记(CMS initial mark)
仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。
2.并发标记(CMS concurrent mark)
进行GC RootsTracing的过程。
3.重新标记(CMS remark)
修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,
这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
4.并发清除(CMS concurrent sweep)
其中,初始标记、重新标记这两个步骤仍然需要Stop The World。
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起
工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
G1收集器
G1(Garbage-First)收集器是当今收集器技术发展的最前沿成果之一,早在JDK 1.7刚刚
确立项目目标,Sun公司给出的JDK 1.7 RoadMap里面,它就被视为JDK 1.7中HotSpot虚拟机
的一个重要进化特征。从JDK 6u14中开始就有Early Access版本的G1收集器供开发人员实
验、试用,由此开始G1收集器的“Experimental”状态持续了数年时间,直至JDK 7u4,Sun公
司才认为它达到足够成熟的商用程度,移除了“Experimental”的标识。
G1是一款面向服务端应用的垃圾收集器。HotSpot开发团队赋予它的使命是(在比较长
期的)未来可以替换掉JDK 1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特
点。
并行与并发
G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者
CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的
GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
分代收集
与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其
他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已
经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
空间整合
与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实
现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这
两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种
特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一
次GC。
可预测的停顿
这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,
但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一
个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实
时Java(RTSJ)的垃圾收集器的特征了。
在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这
样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分
为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和
老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java
堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的
空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时
间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分
内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高
的收集效率。
G1把内存“化整为零”的思路,理解起来似乎很容易,但其中的实现细节却远远没有想象
中那样简单,否则也不会从2004年Sun实验室发表第一篇G1的论文开始直到今天(将近10年
时间)才开发出G1的商用版。笔者以一个细节为例:把Java堆分为多个Region后,垃圾收集
是否就真的能以Region为单位进行了?听起来顺理成章,再仔细想想就很容易发现问题所
在:Region不可能是孤立的。一个对象分配在某个Region中,它并非只能被本Region中的其
他对象引用,而是可以与整个Java堆任意的对象发生引用关系。那在做可达性判定确定对象
是否存活的时候,岂不是还得扫描整个Java堆才能保证准确性?这个问题其实并非在G1中才
有,只是在G1中更加突出而已。在以前的分代收集中,新生代的规模一般都比老年代要小许
多,新生代的收集也比老年代要频繁许多,那回收新生代中的对象时也面临相同的问题,如
果回收新生代时也不得不同时扫描老年代的话,那么Minor GC的效率可能下降不少。
在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象
引用,虚拟机都是使用Remembered Set来避免全堆扫描的。G1中每个Region都有一个与之对
应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个
Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代
的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过
CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内
存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗
漏。
如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:
初始标记(Initial Marking)
并发标记(Concurrent Marking)
最终标记(Final Marking)
筛选回收(Live Data Counting and Evacuation)
对CMS收集器运作过程熟悉的读者,一定已经发现G1的前几个
步骤的运作过程和CMS有很多相似之处。
初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改
TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的
Region中创建新对象,这阶段需要停顿线程,但耗时很短。并发标记阶段是从GC Root开始
对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执
行。而最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动
的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最
终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线
程,但是可并行执行。最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序,
根据用户所期望的GC停顿时间来制定回收计划,从Sun公司透露出来的信息来看,这个阶段
其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控
制的,而且停顿用户线程将大幅提高收集效率。通过图3-11可以比较清楚地看到G1收集器的
运作步骤中并发和需要停顿的阶段。
运行示意图
参考
《深入理解java虚拟机》