垃圾收集器就是使用一种或者多种垃圾回收算法,在不同的内存分配策略下,进行垃圾回收的程序。由于内存分配策略不同的原因,所以垃圾回收算法以及垃圾收集器的种类也不尽相同,下图是JDK1.7 Update 14之后的HotSpot虚拟机所包含的所有的垃圾收集器。
HotSpot中的收集器分类
图上总体分为两大类,年轻代(Young generation)和老年代(Tenured generation)。上图中的连线表示在年轻代和老年代之间,哪些收集器可以两两配合使用,“八字不合”的收集器是不能在一起的!注意到,G1收集器是当前最前沿的收集器,它不需要和其他收集器配合,自己就能完成年轻代和老年代的垃圾回收工作。
1.Serial收集器
Serial收集器应该算是收集器家族中元老级的任务,它是一款历史比较悠久的收集器,在JDK1.3之前还是新生代收集器的唯一选择。Serial的中文意思是“串行”,也就是说他是一个单线程工作的垃圾收集器,其所有的工作都是在单个线程单个CPU的情况下完成的。除了单线程之外,Serial收集器在进行垃圾回收的过程中,需要暂停其他所有的用户线程,JAVA专家称这种情况为:“Stop The World”。这种比如真的是再形象不过了,因为在那一刻真的好像世界都停止了。有人举过一个形象的例子,你女朋友在给你打扫放假的时候,肯定不会让你再到处乱跑,如果她一边打扫,你一边乱跑,很可能晚上就要睡沙发了。实际上,“Stop The world”导致的停顿现象,即便是现在的最新的G1垃圾收集器也不可能说完全避免,而只能不断地缩短Stop The World的时间。
通过上面的介绍,大家可能觉得Serial收集器可能是一个又老又慢过时的收集器,估计已经被淘汰了。然而事实上并非如此,它到目前为止依赖是虚拟机运行在client模式下的默认年轻代收集器。我们先来看一下Serial收集器的工作的过程,之后可以和后面的几个收集器在进行对比,就能发现Serial收集器在某些特殊的环境下,还是有优点的:
Serial/Serial Old收集器回收过程
正式因为Serial的单线程,所以它相比于其他的收集器来说,优点就是简单而高效,对于单个CPU的环境的来说,Serial收集器没有线程交互,所以回收的效率相对来说就比较高。所以Serial收集器更适合运行在client模式下的虚拟机,如桌面应用等。
关于JVM Client模式和Server模式:Client模式采用的是轻量级的虚拟机,所有启动的比较快;Server模式采用的是重量级的虚拟机,所以启动比较慢。重量级的虚拟机在运行期间做了很多的优化,所以启动之后程序运行比较快。Client模式一般内存占用的情况比较小,VM在client模式默认-Xms是1M,-Xmx是64M;JVM在Server模式默认-Xms是128M,-Xmx是1024M;所以Client在进行Stop The World的时间相对来说不会太长,所以使用简单快速的Serial收集器来进行垃圾回收时再合适不过的了。
2.ParNew收集器
ParNew名字前面的Paral实际上是单词“parallel”的缩写,意思是并行。所以从名字就可以知道ParNew收集器应该是多线程,事实上它就是Serial收集器的多线程的版本,所以,它除了是多线程的之外,其他的所有的东西像控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样。ParalNew收集器回收过程如下图所示:
ParNew / Seria Old收集器回收过程
ParNew收集器相对于Serial收集器除了多线程之外,并没有其他的特点。所以如果在单CPU的情况下,ParalNew收集器并不会比Serial的性能好,甚至因为存在线程的交互的开销,ParalNew性能反而更差。另外就是,在JDK1.5时期推出了CMS(Concurrent Mark Sweep),这个几乎被认为有划时代意义的垃圾收集真正实现了垃圾回收的并发操作,也就是用户线程和GC线程是同时工作的。但是大家从开头的分类图上可以看出,在新生代中,可以和CMS同时工作的只有Serial和ParNew可以与之搭配工作。所以,在多核的环境下,ParNew收集器是最佳的选择。
3.Parallel Scavenge收集器
Parallel Scavenge收集器是一个新生代收集器,它也是使用了复制算法的收集器(前面介绍的两种收集器都是复制算法),而且又是并行的多线程收集器,看起来好像和ParNew收集器没什么不同。哲学上常说,存在即是合理的,一件东西不会平白无故的出现,Parallel Scavenge也是一样,那么它到底有什么神奇之处呢?
Parallel Scavenge收集器的特点是它的关注点和其他的收集器不一样,CMS收集器的关注点是尽可能是缩短垃圾回收时用户线程的停顿时间,而Parallel收集器的目标则是达到一个可控制的吞吐量。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值:
停顿时间越短越适合与用户交互的程序,因为与用户交互的程序需要有良好的响应速度来提升用户的体验。也就是说,在一系列的用户操作中,用户并不希望出现长久停顿的情况。就好比你正在看某种大片,过程中每隔一段时间就停顿一下,你肯定很不爽。假如这个停顿的时间就是垃圾回收导致的,所以你当然希望这个停顿的时间越短越好。Parallel Scavenge就是用来将吞吐量控制在一个可以接受的范围内。Parallel Scavenge提供几个参数来精确控制吞吐量:
参数 | 参数描述 |
-XX:MaxGCPauseMillis | 该参数允许设置一个大于0的毫秒数,收集器将尽可能地保证 内存花费的时间不超过该值 |
-XX:GCTimeRatio | 该参数允许设置一个0~100之间的整数,也就是垃圾收集时间占用比,相当于吞吐量的倒数 |
-XX:+UseAdaptiveSizePolicy | 该参数是一个开关参数,打开之后就不需要制定新生代的大小(-Xmm),Eden与Suirvive的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PreTenureSizeThreshold)等细节参数了,虚拟机会根据系统运行情况,动态调整这些参数 |
注意:虽然Paralle Scavenge的参数是可以精确控制程序运行的吞吐量的,但是并不是GC停顿时间控制越小越好。因为GC停顿时间变短,是以牺牲GC吞吐量和新生代空间来换取的。也就是说,过小的GC停顿时间会导致更频繁地发生GC。所以,Paralle Scavenge参数要设置在一个合理的范围内,才能让系统更好的运行。如果对于这方面没有经验的人,可以使用参数3,让系统自己决定吞吐量。
4.Serial Old收集器
Serial Old收集器相相当于Serial收集器的老年代版本,使用的是标记-整理的算法,同样他是一个单线程的收集器。现在这个收集器的主要作用就是在Client模式下,作为虚拟机老年代的回收的垃圾收集器使用。但是如果你要把用在Server模式中,他就具备下面两种用途:
- 在JDk1.5之前包括1.5的版本,与Parallel Scvenge 收集器搭配使用;
- 作为CMS收集器的后备方案,也就是并发收集发生Conrurrent Mode Failure时作为兜底的方案使用。
Serial Old收集器的工作过程如下图所示:
ParNew / Seria Old收集器回收过程
5.Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用的是多线程和标记-整理。这个算法是在JDK1.6之后才提供的,所以Serial Old才可以作为Parallel Scavenge收集器Jdk1.5之前的年轻代收集器配合使用。所以在JDK1.6出现之前,Parallel Scavenge收集器是比较尴尬的,一方面自己在年轻代中使用的是多线程的,而在老年代中,只有单线程的Serial Old可以选择。所以在Parallel Old收集器出现之前,Parallel Scavenge收集器的地位是十分尴尬的。由于在老年代中Serial Old的效率拖后腿,所以Parallel Scavenge收集器一直处于一种不给力的状态。
直到Parallel Old收集器出现之后,“吞吐量优先”才有了比较名副其实的应用组合,所以JDk1.6之后,在注重吞吐量以及CPU资源敏感的场景中,都可以优先考虑Parallel Scavenge加Parallel Old的组合。他们组合的垃圾回收过程如下图所示:
Parallel Scavenge / Parallel Old收集器回收过程
5.CMS收集器(Concurrent Mark Sweep)
CMS收集器的目标是以获得最短回收停顿时间为目标的收集器(注意和Parallel Old收集器加以区分,Parallel Old是目标是精确控制程序运行的吞吐量,即停顿时间可控制)。从名称上我们可以看出来CMS使用的标记-清除的算法,它的运行过程相对来说比较复杂,整个过程可以分为4个步骤:
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)
- 并发清除(CMS concurrent sweep)
我们可以先来看一下CMS收集器的垃圾回收过程,如下图所示:
CMS垃圾回收过程
从图上可以看出,在初始标记和重新标记这两个阶段仍然需要"Stop The World"。初始标记只是标记一下GC Roots能直接关联到的对象,速度很快。并发标记的阶段就是进行GC Roots Tracing的过程,而重新标记的过程就是为了修正并发标记期间因为用户程序仍然运行而导致变动的那一部分对象的标记记录。这个过程中的时间要比初始标记长一些,但是要远比并发标记的时间要短。所以CMS把标记阶段可以大致理解为两个阶段,一个阶段是可以并发标记的阶段,不会影响程序运行;另一个阶段就是不可以并发标记的阶段,就是标记过程会影响程序运行,所以这个阶段要“Stop The World”。但是因为大部分标记工作已经在第一个阶段完成,所以第二部分的时间就非常短。相比于之前的收集器不加以区分的方式,这种方式可以大大减少因为标记而导致“Stop The World”停顿的时间。
从设计来说,CMS使用的也是多线程的方式来进行垃圾回收,由于是和用户线程同时进行,且使用的标记-清除算法,所以它就存在以下三个明显的缺点:
- CMS默认启动的回收线程数是(CPU数量+3)/4,也就是说当CPU数量在4个以上的时候,并发回收时垃圾收集线程数量不少于25%,并且随着CPU数量的增加而下降。但是当CPU数量不足于4个时,CMS对于用户程序影响就比较大,因为很可能有不少于一半的线程用来垃圾回收,等于性能一下子降了一半,是无法忍受的。所以,使用CMS收集器最好是在4以上的CPU数量的环境下才能有很好的体验。
- CMS收集器无法处理浮动垃圾,浮动垃圾听着好像是很吊的东西,其实也很好理解。就是因为在并发清理的过程是和用户线程同时并发进行的,所以在清理的过程中,会产生新的垃圾,这些新的垃圾已经不能立即回收,只能等到下次垃圾回收的时候再清理。等于说,有一段时间这些没有用内存空间被浪费掉。而且由于在垃圾清理的阶段,用户程序在运行,所以老年代中必须要预留一段空间给用户线程使用。这里要涉及到一个参数配置:-XX:CMSInitiationOccupancyFraction,这个参数表示老年代使用了多少百分比的时候激活CMS收集器。如果这个参数设置的比较高,那么CMS垃圾回收的频率就要低,老年代中的浮动垃圾就会滞留的比较大,那么导致“Concurrent Mode Failure”的几率就比较大,因为老年代剩余的空间不足于满足用户线程的运行,所以会触发一次Full GC(也就是前面提到的,会使用CMS后背方案,也就是用Serial Old垃圾收集器来进行一次老年代的垃圾回收),这种情况下停顿的时间就会比较长。频繁的发生这种事情,性能就会降低,所以这个参数设置对于CMS性能至关重要。
- CMS使用的是标记-清理的算法来进行垃圾回收的,了解标记-清理的算法肯定知道,这种算法会导致空间碎片。如果空间碎片过多,那么在进行大对象空间分配的时候很可能不能满足大对象的连续空间的要求,从而导致了一次Full GC。为了解决这个问题,CMS收集器提供了一个参数:-XX:UseCMSCompactAtFullCollection,这是一个开关,用来控制CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程。这个过程虽然解决了内存碎片的问题,但是由于内存碎片整理的过程中是无法并发,所以“Stop The World”停顿的时间不得不变长。还有一个参数:-XX:CMSFullGCsBeforeCompaction,这个参数表示执行多少次不压缩(即不整理空间随便)Full GC后,进行一次带压缩的Full GC。
5.G1收集器(Garbage First)
G1是一款面向服务端应用的垃圾收集器,HotSpot团队的目标是,在未来可以用G1替换掉JDK1.5中发布的CMS收集器。与其他的收集器相比,G1收集器具有如下特点:
- 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(多个CPU或者是多个CPU核心)来缩短“Stop The World”的停顿时间,部分其他收集器原本需要停顿java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续运行。
- 分代收集:与其他收集器一样,分代概念在G1中仍然得意保留。虽然G1收集器不需要和其他的收集器配合就能管理整个GC堆,但是它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过了多次GC的旧对象,以期通过这种方式获得更好的垃圾回收的效果。
- 空间整合:与CMS的“标记-清理”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器,从局部上来看是基于“复制”算法实现的。不管使用这两种的哪种,就不会产生内存碎片。
- 可预测的停顿:这是G1对象CMS的另一大优势,二者虽然都关注降低停顿时间,但是G1除了关注降低停顿时间,但是G1处理这点之外,还可以建立可预测的停顿时间模型,能让使用者明确指定一个长度在M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
在G1之前的其他收集器进行瘦的范围都是整个新生代和老年代,G1收集器java堆的内存布局与其他收集器有很大的差别,他将整个java堆分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,他们都是一部分Region(不需要连续)的集合。
G1的详解讲解会作为新的一章来分析,以上关于G1的描述摘抄子(深入了解虚拟机),后面会有单独的一章来讲G1收集器。