几种常用的垃圾收集算法:标记 - 清除算法、复制算法、标记 - 整理算法、分代收集算法。
“ 标记 - 清除 ” (Mark - Sweep)算法
“ 标记 - 清除 ” (Mark - Sweep)算法是最基础的收集算法,它分为 “ 标记 ” 和 “ 清除 ” 两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象 。
它的主要不足有两个:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
复制算法(回收新生代)
“ 复制 ” (Copying)收集算法将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor 。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。
HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1,也就是说每次新生代中可用内存空间为整个新生代容量的 90%,只有 10% 的内存会被 “ 浪费 ” 。
不过因为我们没有办法保证每次回收都只有不多于 10% 的对象存活, 当 Survivor 空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。
标记 - 整理算法(回收老年代)
首先标记出所有需要回收的对象,让所有存活的对象都向一端移动(整理),然后直接清理掉端边界以外的内存。
分代收集算法
“ 分代收集 ” (Generational Collection)算法根据对象存活周期的不同将内存划分为几块,一般是把 Java 堆分为新生代和老年代。在新生代中,每次垃圾收集时只有少量的对象存活,所以在新生代中使用复制算法。在老年代中因为对象存活率高、没有额外的空间对它进行分配担保,就必须使用 “ 标记 - 清理 ” 或者 “ 标记 - 整理 ” 算法来进行回收。
垃圾收集器是内存回收的具体实现,HotSpot 虚拟机的垃圾收集器有 7 种,分别为 Serial 、ParNew 、Parallel Scavenge 、CMS 、Serial Old(MSC)、Parallel Old 、G1 收集器 。
Serial 收集器
Serial 收集器是最基本的收集器,它是一个单线程的收集器,在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(称为 Stop The World)。
Serial 收集器的优点:简单而高效(与其他收集器的单线程比);由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率 。
ParNew 收集器
Parnew 收集器其实就是 Serial 收集器的多线程版本 。除了使用多条线程进行垃圾收集之外(并行),其余动作包括 Serial 收集器可用的所有控制参数、收集算法 、Stop The World 、对象分配规则 、回收策略 等都与 Serial 收集器完全一样 。
除了 Serial 收集器外,目前只有 ParNew 收集器可与 CMS 收集器配合工作。
Parallel Scavenge 收集器
Parallel Scavenge 收集器其它与 ParNew 收集器一样,只是比 ParNew 收集器多了一个可以控制吞吐量(Throughput)大小的功能。所谓吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即吞吐量 = 运行用户代码的时间 / (运行用户代码时间 + 垃圾收集时间)。
GC 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验;而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。这两者是相互矛盾的,GC 停顿时间的缩短是以牺牲吞吐量和新生代空间来换取的。
Serial Old 收集器
Serial Old 收集器是 Serial 收集器的老年代版本。同样是单线程收集器,使用 “ 标记 - 整理 ” 算法。
Parallel Old 收集器
Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,使用多线程和 “ 标记 - 整理 ” 算法,与 Parallel Scavenge 收集器形成 “ 吞吐量优先 ” 的应用组合,在注重吞吐量以及 CPU 资源敏感的场合可以优先考虑这对组合。
CMS 收集器(新生代)
CMS (Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它的内存回收过程是与用户线程一起并发执行的,所以官方文档也称之为并发低停顿收集器,基于 “ 标记 - 清理 ” 算法实现,整个运作过程分为四个步骤:
1)初始标记(CMS inital mark):Stop The World,仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快。
2)并发标记(CMS concurrent mark):进行 GC Roots Tracing 的过程。
3)重新标记(CMS remark):Stop The World,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这一阶段的停顿时间也远比并发标记的时间短,不会停顿太久 。
4)并发清除(CMS concurrent sweep)
CMS 收集器有以下三个明显的缺点:
1)CMS 收集器对 CPU 资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说是 CPU 资源)而导致应用程序变慢,总吞吐量会降低。
2)CMS 收集器无法处理浮动垃圾(Floating Garbage),可能出现 “ Concurrent Mode Failure ” 失败而导致另一次 Full GC 的产生。
由于 CMS 并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理掉它们,只好等待下一次 GC 时再清理掉。这一部分垃圾就是浮动垃圾。
3)CMS 是一款基于 “ 标记 - 清除 ” 算法实现的收集器,这意味着收集结束时会有大量空间碎片产生,给对象分配带来很大麻烦。往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次 Full GC 。
G1 收集器
G1(Garbage - First)收集器是一款面向服务端应用的垃圾收集器,具备以下特点:
1)并发与并行
2)分代收集:G1 可以独立管理整个 GC 堆(新生代和老年代)
3)空间整理:G1 从整体来看是基于 “ 标记 - 整理 ” 算法实现的收集器,从局部(两个 Region 之间)上来看是基于 “ 复制 ” 算法实现的,收集后不会产生内存空间碎片。
4)可预测的停顿:G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。
使用 G1 收集器时,Java 堆的内存布局就与其他收集器有很大区别,它将整个 Java 堆划分为多个大小相等的独立区域(Region)。G1 收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集。G1 跟踪各个 region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region (这也就是 Garbage - First 名称的由来)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率。
G1 收集器的运作大致可以分为以下几个步骤:
1)初始标记(Initial Marking)
2)并发标记(Concurrent Marking)
3)最终标记(Final Marking):为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
4)筛选回收(Live Data Counting and Evacuation):首先对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。