之前我们已经讲过了垃圾收集算法,如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。JDK 1.7 Update 14 之后的 HotSpot 虚拟机包含的收集器如下图:
如果两个收集器之间存在连线,就说明它们可以搭配使用。虚拟机所处的区域,则表明它是属于新生代收集器还是老年代收集器。
Serial 收集器
一个单线程的新生代收集器,在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
因为简单高效,同时对于收集比较小的空间的新生代停顿时间很短,完全可以接受,故适合使用在虚拟机Client模式下。
ParNew 收集器
ParNew 收集器其实就是 Serival 收集器的多线程版本。
它是许多运行在 Server 模式下的虚拟机中首先的新生代收集器,其中一个很重要的原因就是,除了 Serial 收集器为,目前只有它能与 CMS 收集器配合工作。
Parallel Scavenge 收集器
Parallel Scavenge 收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器,到这里与 ParNew 是一样。
但是 Parallel Scavenge 收集器的特点是它的关注点与其他收集器不同,其他收集器的关注点是如何缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量。所谓吞吐量就是 CPU 用于运行用户代码时间与 CPU 总消耗时间的比值,即吞吐量=运行用户代码时间/ (运行用户代码时间 + 垃圾收集时间),虚拟机共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是 99% 。
停顿时间越短越适合需要与用户交互的程序,而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
Parallel Scavenge 收集器还有一个与 ParNew 收集器的重要区别就是它可以设置 GC 自适应的调节策略(虚拟机根据当前系统运行情况收集性能监控信息,动态调整新生代大小、晋升老年代对象年龄等细节参数,以提供最合适的停顿时间或者最大的吞吐量)。
Serial Old 收集器
Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用「标记-整理」算法。这个收集器的主要意义也是在于给 Client 模式下的虚拟机使用。
Parallel Old 收集器
Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程和「标记-整理」算法。
在 Parallel Old 收集器出现之前,如果新生代选择了 Parallel Scavenge 收集器,老年代则只能选择 Serial Old 收集器,而由于老年代的 Serial Old 收集器在服务端应用性能上的「拖累」,整理应用上未必能获得吞吐量最大化的效果。
CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一种以获得最短回收停顿时间为目标的收集器,适用于重视服务的响应速度,系统停顿时间最短,以给用户更好的体验。
CMS 收集器是基于「标记-清除」算法实现的,整个过程分为 4 个步骤:
-
初始标记
-
并发标记
-
重新标记
-
并发清除
其中,初始标记、重新标记这两个步骤仍然需要暂停所有用户线程。
初始标记:用来标记一个 GC Root 能直接关联到的对象,并标记,这个阶段需要暂停所有用户线程(Stop The World),不过速度很快。
并发标记:在初始标记的基础上继续向下追踪并标记能关联到的对象,这个线程和应用程序的线程并发执行。
重新标记:重新编辑阶段是为了修正并发标记期间因用户程序继续运行而导致标记产生变化的那一部分对象的标记记录,这个阶段同样需要暂停所有用户线程(Stop The World)。
并发清除:这个阶段进行垃圾对象的清理,收集器线程和应用程序的线程并发执行。
CMS 的主要优点就是并发收集、低停顿,但是它仍有如下几个缺点:
-
需要更多的 CPU 资源。因为并发阶段,虽然不厚导致用户线程停顿,但是仍会因为占用了一部分 CPU 资源而导致应用程序变慢。
-
需要更多的堆空间。因为 CMS 标记阶段应用程序仍在运行,可能会在回收结束前出现内存不足的情况,所以不能在老年代满了的时候才开始收集,必须预留出一部分空间。
-
空间碎片问题。因为 CMS 是基于「标记-清除」算法,收集结束时会有大量空间碎片产生,一旦空间碎片过多,会给大对象分配带来很大的麻烦。
G1 收集器
G1 是一款面向服务端应用的垃圾收集器,与其他 GC 收集器相比,G1 具备如下特点:
-
并发与并行:G1 充分利用多 CPU、多核环境下的硬件优势,使用多个 CPU 来缩短 Stop-The-World的时间。部分其他收集器原本需要停顿 Java 线程执行 GC 动作,G1 收集器仍可以通过并发让 Java 程序继续执行。
-
分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是分代的概念仍然得以保留,它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次 GC 的旧对象以获取更好的收集效果。
-
空间整合:与 CMS 的「标记-整理」算法不同,G1 从整体看是基于「标记-整理」算法实现的收集器,从局部(两个 Region 之间)上看是基于「复制」算法实现的。这两种算法都意味着 G1 运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。
-
可预测的停顿:这是 G1 相对于 CMS 的另一个优势,但 G1 除了追求停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片断内,消耗在垃圾收集上的时间不得超过 N 毫秒。
在 G1 之前的其他收集器进行收集的范围都是整个新生代或者老年代,而 G1 不再是这样。在使用 G1 收集器时,它将整个 Java 堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分 Region(不需要连续)的集合。每次根据允许的收集时间,优先回收价值最大的 Region(这也就是 Garbage-First 名称的由来)。
G1 收集器的运作大致可划分为以下几个步骤:
-
初始标记
-
并发标记
-
最终标记
-
筛选回收
前三个步骤与 CMS 收集器的运作过程基本一致,最后在筛选回收阶段首先对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。
垃圾收集器参数总结
参数 | 描述 |
---|---|
UseSerialGC | 虚拟机运行在 Client 模式下的默认值,使用 Serial + Serial Old 的收集器组合 |
UseParNewGC | 使用 ParNew + Serial Old 的收集器组合 |
UseConcMarkSweepGC | 使用 ParNew + CMS + Serial Old(CMS失败后备用的)收集器组合 |
UseParallelGC | 虚拟机运行在 Server 模式下的默认值,使用 Parallel Scavenge + Serial Old组合 |
UseParallelOldGC | 使用 Parallel Scavenge + Parallel Old 收集器组合 |
SurvivorRatio | 新生代中 Eden 区域与 Survivor 区域的容量比值,默认为 8 |
PretenureSizeThreshold | 直接晋升到老年代的对象大小,大于这个参数的对象将直接分配在老年代 |
MaxTenuringThreshold | 晋升到老年代的对象年龄,每个对象在坚持过一次 Minor GC 后,年龄加 1 |
UseAdaptiveSizePolicy | 动态调整 Java 堆中各个区域的大小以及进入老年代的年龄 |
HandlePromotionFailure | 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代所有对象都存活的极端情况 |
ParallelGCThreads | 设置并行 GC 时进行内存回收的线程数 |
GCTimeRatio | GC 时间占总时间的比率,默认值为 99,即允许 1% 的 GC 时间。仅在使用 Parallel Scavenge 收集器时生效 |
MaxGCPauseMillis | 设置 GC 的最大停顿时间。仅在使用 G1 收集器时生效 |
CMSInitiatingOccupancyFraction | 设置 CMS 收集器在老年代空间被使用多少后触发垃圾回收。默认为 68%,仅在使用 CMS 收集器时生效 |
UseCMSCompactAtFillCollection | 设置 CMS 收集器在完成垃圾收集后是否进行一次内存碎片整理 |
CMSFullGCsBeforeCompaction | 设置 CMS 收集器在进行若干次垃圾收集后再启动一次内存碎片整理 |