经典收集器
虽然我们会对各个收集器进行比较,但并非为了挑选一个最好的收集器出来。垃圾收集器的技术在不断进步,
但直到现在还没有最好的收集器出现,更加不存在“万能”的收集器,所以我们选择的只是对具体应用最合适的收集器。
图1-1展示了七种作用于不同分代的经典收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用,
图中收集器所处的区域,则表示它是属于新生代收集器抑或是老年代收集器。
图1-1 HotSpot虚拟机的垃圾收集器
Serial收集器
只看名字就能够猜到,Serial收集器是一个单线程工作的收集器,但它的“单线程”的意义并不仅仅是
说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,
必须暂停其他所有工作线程,直到它收集结束。
图1-2 Serial/Serial Old收集器运行示意图
迄今为止,它依然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器,有着优于其他收集器的地方,
那就是简单而高效(与其他收集器的单线程相比),对于内存资源受限的环境,它是所有收集器里额外内存消耗最小的。
ParNew收集器
ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之
外,其余的行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:
PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规
则、回收策略等都与Serial收集器完全一致,ParNew收集器的工作过程如图1-3所示。
图1-3 ParNew/Serial Old收集器运行示意图
在JDK 5发布时,HotSpot推出了一款在强交互应用中几乎可称为具有划时代意义的垃圾收集器——CMS收集器。
这款收集器是HotSpot虚拟机中第一款真正意义上支持并发的垃圾收集器,它首次实现了让垃圾收集线程与用户线程(基本上)同时工作。
遗憾的是,CMS作为老年代的收集器,却无法与JDK 1.4.0中已经存在的新生代收集器ParallelScavenge配合工作,
所以在JDK 5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。
ParNew收集器是激活CMS后(使用-XX:+UseConcMarkSweepGC选项)的默认新生代收集器,也可以使用-XX:+/-UseParNewGC选项来强制指定或者禁用它。
可以说直到CMS的出现才巩固了ParNew的地位。
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为
关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。CMS收集器就非常符合这类需求。
CMS收集器是基于标记-清除算法实现的,运作过程相对要更复杂一些,整个过程分为四个步骤,包括:
- 初始标记(CMS initial mark), 标记一下GCRoots能直接关联到的对象,速度很快,会导致程序短暂停顿。
- 并发标记(CMS concurrent mark),从GC Roots的直接关联对象开始遍历整个对象图的过程,
这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。 - 重新标记(CMS remark),修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,会导致程序短暂停顿。
- 并发清除(CMS concurrent sweep),清理删除掉标记阶段判断的已经死亡的对象。
由于在整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一起工作。
从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。通过图1-4可以看到CMS收集器的运作步骤中并发和需要停顿的阶段。
图1-4 CMS 收集器运行示意图
Garbage First收集器
Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路
和基于Region的内存布局形式。
在G1收集器出现之前的所有其他收集器,包括CMS在内,垃圾收集的目标范围要么是整个新生代(Minor GC),
要么就是整个老年代(Major GC),再要么就是整个Java堆(Full GC)。
而G1则不然,它可以面向堆内存任何部分来组成回收集(Collection Set)进行回收,衡量标准不再是它属于哪个分代,
而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。
虽然G1也是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,
而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden、Survivor,或老年代空间。
收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经熬过多次收集的旧对象都能获取很好的收集效果。
Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。
每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。
而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,
G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待,如图1-5所示。
图1-5 G1收集器Region分区示意图
相较之CMS,G1更占堆内存,占用整个堆容量的20%甚至更多,目前在小内存应用上CMS的表现大概率仍然要会优于G1,
而在大内存应用上G1则大多能发挥其优势,这个优劣势的Java堆容量平衡点通常在6GB至8GB之间。