垃圾回收算法
基础算法
标记-清除算法
顾名思义,先标记可回收的对象,再统一进行回收清除。

优点:实现简单 缺点:效率低下,容易产生大量不连续的内存碎片
标记-复制算法
该算法将内存分为两块空间,每次只使用一块,标记后,将存活对象复制到另一块内存中,将原先使用的内存块全部清除当做新的保留空间。

优点:实现简单,不会产生内存碎片 缺点:浪费一半内存空间
备注:
现在的商用Java虚拟机大多都优先采用了这种收集算法去回收新生代,IBM公司曾有一项专门研 究对新生代"朝生夕灭"的特点做了更量化的诠释------新生代中的对象有98%熬不过第一轮收集。因此 并不需要按照1∶1的比例来划分新生代的内存空间。
HotSpot虚拟机中,将新生代可以使用的内存分为一块Eden
区和两块Survivor
区,空间大小比例为8:1:1,类似于标记-复制算法描述的那样,每次只使用一块Eden
区和一块Survivor
区,对象回收后,将存活对象都复制到保留的那一块Survivor
区中,由于这块Survivor
区仅占10%,因此如果大多数对象都存活的话,显然这块小空间是不够用的,对于此种情况,虚拟机提供了一种内存分配担保的机制,即如果这块Survivor
区空间不够用,那么一些对象会直接进入到老年代。
标记-整理算法
标记-整理算法是针对老年代的消亡特征所涉及的算法,由于老年代对象大多在一次FullGC之后都会存活下来,因此不适合使用标记-复制算法中的内存空间分配若干个小块的方案,如此必然需要提供额外的一块空间以进行内存担保。标记-整理算法的标记和上述两个算法的流程是类似的,但是后续的流程是将所有存活对象往内存一侧移动,之后将边界以外的内存全部清理。

优点:不浪费空间,无需使用额外空间担保,不会产生内存碎片。
缺点:移动过程需消耗大量CPU资源,并且需要暂停用户程序,即Stop-The-World。
垃圾收集器
Serial/Serial Old收集器

概览:
名字 | 作用代 | 算法 | 推出的JDK版本 |
---|---|---|---|
Serial | 新生代 | 标记-复制 | JDK 1.3 |
Serial Old | 老年代代 | 标记-整理 | JDK 1.3 |
缺点:单线程,运行时需要暂停用户线程
优点:实现简单,高效(相对于其他GC的单线程版本)
ParNew收集器

概览:
名字 | 作用代 | 算法 | 推出的JDK版本 |
---|---|---|---|
ParNew | 新生代 | 标记-复制 | 1.4 |
Serial
收集器的多线程版本,也是一款新生代垃圾回收器,需要搭配Serial Old
收集器(老年代收集器)使用。
缺点:运行时需要暂停用户线程
优点:多线程协作,更高效
Parallel Scavenge / Parallel Old收集器
概览:
名字 | 作用代 | 算法 | 推出的JDK版本 |
---|---|---|---|
Parallel Scavenge | 新生代 | 标记-复制 | jdk 1.4 |
Parallel Old | 老年代 | 标记-整理 | jdk 6 |
Parallel Scavenge
总体上与ParNew
收集器相似,同样也是多线程运作,但不像CMS这些垃圾收集器关注的是如何减少垃圾收集时用户线程的停顿时间,Parallel Scavenge关注的是吞吐量(Throughput),因此又被称为吞吐量优先收集器。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值。这种收集器适用于离线任务较多的程序,可以充分利用CPU等资源,不适合注重低延迟的程序。
<math xmlns="http://www.w3.org/1998/Math/MathML"> 吞吐量 = 用户程序执行时间 用户程序执行时间 + 垃圾收集时间 \text{吞吐量} = \frac{\text{用户程序执行时间}}{\text{用户程序执行时间} + \text{垃圾收集时间}} </math>吞吐量=用户程序执行时间+垃圾收集时间用户程序执行时间
这里有一个误区,看起来减少垃圾收集时间不就可以增加吞吐量了吗?那么Parallel Scavenge
收集器的关注点不还是停顿时间吗?单次垃圾收集来看确实如此,但是垃圾收集停顿时间的缩短是以减少新生代空间为代价的(就跟扫地一样,房间面积小,肯定扫的更快),但是会让垃圾收集的频率提高,从而总共的停顿时间也增加了,最终反而导致吞吐量下降了。因此Parallel Scavenge
总是会在垃圾收集时间和频率之间进行权衡,以逼近用户通过参数 -XX:GCTimeRatio设置的吞吐量指标(实际上是吞吐量的倒数)。
拓展:
Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy值得我们关注。这是一 个开关参数,当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区 的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数 了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。这种调节方式称为垃圾收集的自适应的调节策略(GC Ergonomics)。
在jdk6之前,Parallel Scavenge
只能与Serial Old
老年代垃圾收集器搭配使用,jdk6推出了Parallel Old
,作为Parallel Scavenge
收集器的老年代版本,自此新生代和老年代都可以多线程地进行垃圾回收了,Parallel Scavenge
也不用像之前那样被单线程的Serial Old
拖累了。
CMS收集器
概览:
名字 | 作用代 | 算法 | 推出的JDK版本 |
---|---|---|---|
CMS((Concurrent Mark Sweep) | 老年代 | 标记-清除 | jdk 5 |

如图所示,CMS
回收期分为4个步骤:
- 初始标记 此阶段需要暂停所有用户线程,主要做的工作是标记一下GC Roots直接关联的对象,所以速度较快
- 并发标记 此阶段可以与用户线程并发执行,用户程序无需停止,该阶段做的主要工作是从GC Roots直接关联对象 开始扫描整个对象图,耗时较长,但是不影响用户程序的执行
- 重新标记 由于上个阶段用户程序和标记线程并发的执行,所以对象引用可能会发生变化,因此需要重新标记,此阶段需要暂停所有用户线程,但由于这个时间段内的引用变化相对较少,且重新标记可以多线程执行,因此速度相对较快
- 并发清理 该阶段用来清理标记的可回收对象,可与用户程序并发执行
可以看到,CMS
之所以可以有效降低垃圾回收停顿时间,是因为其尽可能地让回收过程与用户程序并发执行,能不影响用户程序就不影响,只有在初始标记和重新标记阶段会STW(Stop the world),但是这两个阶段的耗时较短。遗憾的是,CMS
推出时,无法与Parallel Scavenge
新生代回收器配合使用,只能选择与ParNew
或者Serial
搭配使用。
最后,CMS
并非没有缺点,首先CMS
收集器基于标记-清除算法,因此长时间运行后必然会产生大量的内存碎片,当某个大对象在老年代空间没有足够连续内存的时候,就会触发一次Full GC。另外一个问题就是CMS
无法处理浮动垃圾,因为在并发标记阶段和并发清理阶段用户程序都可以同时在运行,所以CMS
不像一般的老年代回收器一样,可以在老年代空间快满时启用,CMS
必须预留足够的空间让用户程序使用,要是CMS
运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次"并发失败"(Concurrent Mode Failure),这时候虚拟机将不得不启动后预案,即临时启用Serial Old
收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了。