Java 堆中几乎存放着 java 中所有的对象实例,垃圾收集器在对堆进行回收前,需要确定哪些对象还”存活”着,哪些已经“死去”。垃圾回收必须能够完成两件事情:正确检测出垃圾对象;释放垃圾对象占用的空间。
1、垃圾检测算法
当前常见的检测垃圾的方法包括两种:1. 引用计数法;2. 可达性分析算法。
1.1 引用计数算法(Reference Counting)
给对象添加一个引用计数器,每当该对象被引用,它的计数器值就+ 1;当引用失效时,计数器就-1;在任何情况下,当计数器值为 0 时,就表示该对象不再被使用。
缺点:它很难解决对象之间相互引用,引起的循环引用问题,会产生无法被释放的内存区域。因此,主流的 JVM 都没有选用引用计数法来管理内存。
1.2 根搜索算法(GC Roots Tracing)
主流的商用程序语言中(Java 和 C=,甚至包括古老的 Lisp),都是使用根搜索算法来判断对象是否存活,通过一系列“GC Roots”的对象作为起始点向下搜索,搜索所走过的路径为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连,则证明此对象是不可用的,如图中 object5, object6, object7 虽然会有关联,但是到 GC Roots 是不可达的,将其判定为可回收的对象。
在 Java 语言中,可作为 GC Roots 的对象包括以下元素:
虚拟机栈(栈帧中的本地变量表)中的引用的对象
方法区中的类静态属性引用的对象;
方法区中的常量引用的对象;
本地方法栈中 JNI 的引用的对象;
对于引用,我们希望能描述这样一类对象:当在内存还足够的时候,能保存在内存之中;如果内存在进行垃圾收集后还是非常紧张,则可抛弃这些对象,很多系统的缓存功能都符合这样的应用场景。从 JDK1.2 版本开始,把对象的引用分为四种级别,从而使程序能更加灵活的控制对象的生命周期,这四种级别由高到低依次为:强引用(StrongReference)、软引用(SoftReference)、弱引用(WeakReference)和虚引用(PhantomReference)。
2、垃圾收集算法
由于垃圾收集算法的实现涉及到大量的程序细节,而且各个平台的虚拟机操作内存的方法又各不同,下面只是介绍几种算法的思想及发展过程。
2.1 标记-清除算法
该垃圾收集算法主要分成”标记“和”清除“两个阶段:首先标记出所有需要回收的对象,而后在标记完成后统一回收所有被标记的对象。
缺点:1. 效率问题,标记和清除两个过程的效率都不高;2. 空间碎片问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一个垃圾回收动作。
2.2 复制算法
为了解决标记-清除存在的效率问题,复制算法将内存划分为相等的两块,每次只使用其中一块。当这一块内存用完时,就将还存活的对象复制到另一块上面,然后将已经使用过的内存空间一次清理掉。
Survivor 上的垃圾回收是这种算法,新生代中的 Eden 和 Survivor 的默认比例是 8:1,所有只有 10%的空间是会被“浪费“的。
缺点:将内存缩小为了原来的一半,对内存空间耗费较大。在对象存活率较高时,需要进行多次复制操作,效率会变低。
2.3 标记-整理算法
将原有标记-清除算法进行改造,不是直接对可回收对象进行清理,而是让所有存活对象都向另一端移动,然后直接清理掉端边界以外的内存。
2.4 分代收集算法
当前商业虚拟机的垃圾回收器采用“分代回收”(Generation Collection)算法,根据对象的存活周期的不同将内存划分成几块,一般把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
在新生代,每次垃圾收集器都发现有大批对象死去,只有少量存活,采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须“标记-清除”或者“标记-整理”算法进行回收。
新创建的对象被分配在新生代,如果对象经过几次回收后仍然存活,那么就把这个对象划分到老年代。老年代的收集频度不象年轻代那么频繁,这样就减少了每次垃圾回收所需要扫描的对象,从而提高了垃圾回收效率。
JVM 将整个堆划分为 Young 区、Old 区和 Perm 区,分别存放不同年龄的对象,这三个区存放的对象有如下区别:
- Young 区分为 Eden 区和两个相同大小的 Survivor 区,其中所有新创建的对象都分配在 Eden 区域中,当 Eden 区域满后会触发 minor GC 将 Eden 区仍然存活的对象复制到其中一个 Survivor 区域中,另外一个 Survivor 区中的存活对象也复制到这个 Survivor 区域中,并始终保持一个 Survivor 区时空的。一般建议 Young 区地大小为整个堆的 1/4。
- Old 区存放 Young 区 Survivor 满后触发 minor GC 后仍然存活的对象,当 Eden 区满后会将存活的对象放入 Survivor 区域,如果 Survivor 区存不下这些对象,GC 收集器就会将这些对象直接存放到 Old 区中,如果 Survivor 区中的对象足够老,也直接存放到 Old 区中。如果 Old 区满了,将会触发 Full GC 回收整个堆内存。
- Perm 区主要存放类的 Class 对象和常量,如果类不停地动态加载,也会导致 Perm 区满。Perm 区地垃圾回收也是有 Full GC 触发地。
3、垃圾收集器
如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图列出了 HotSpot 虚拟机的垃圾收集器,两个垃圾器之间存在连线,就说明它们可以搭配使用。新生代的垃圾回收器包括 Serial、ParNew、Parallel Scavenge,老年代的垃圾回收器包括 CMS、Serial Old、Parallel Old。其中新生代的三种垃圾回收器都采用了复制算法。
3.1 Serial 收集器
Serial 收集器是一个单线程收集器,这个“单线程”不只是说它只会使用一个 CPU 或者一条线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它垃圾收集结束。它对于运行在 client 模式下的虚拟机来说是一个不错的选择
3.2 ParNew 收集器
ParNew 收集器其实就是 Serial 收集器的多线程版本,由于除了 Serial 收集器外,只有它能够与 CMS 收集器配合工作,因此,在运行在 Server 模式下的虚拟机中,ParNew 收集器是首选的新生代收集器。
ParNew 收集器是使用-XX:+UseConcMarkSweepGC 选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC 强制指定。使用-XX:ParallelGCThreads 可以限制垃圾收集的线程数。
3.3 Parallel Scavenge 收集器
这也是一个并行的新生代垃圾收集器,不同于其他收集器(以尽可能缩短垃圾收集时用户线程的停顿时间为目的),它是唯一一个以达到一个可控制的吞吐量为目标的垃圾收集器。
throughput = 运行用户代码的时间 / 总时间(垃圾收集时间+运行用户代码的时间)。
在后台运算的任务中,不需要太多的交互,保证运行的高吞吐量可以高效地利用 CPU 时间,尽快完成程序的运算任务。
Parallel Scavenge 收集器可以使用自适应调节策略,使用-XX:+UserAdaptiveSizePolicy 选项之后,就不需要指定-Xmn、-XX:SurvivorRatio 等参数,虚拟机可以根据当前系统的运行情况动态收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
3.4 Serial Old 收集器
该收集器使用标记-整理算法对老年代垃圾进行回收,它主要的两大用途:1. 配合 Parallel Scavenge 收集器;2. 作为 CMS 收集器在并发收集出现 Concurrent Mode Failure 时使用的后备预案。
3.5 Parallel Old 收集器
Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程和标记整理算法。在注重吞吐量和 CPU 资源敏感的场合,优先考虑使用 Parallel Scavenge + Parallel Old 收集器的组合,切记 Parallel Scavenge 是无法与 CMS 收集器组合使用的。
3.6 Concurrent Mark Sweep 收集器
首先说明下并发与并行的却别:
并行:指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;
并发:指用户线程与垃圾收集线程同时执行。
CMS 收集器是一款并发收集器,是一种以获取最短回收停顿时间为目标的收集器,它是基于标记-清除算法实现的,它整个过程包含四个有效的步骤:
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)
- 并发清除(CMS concurrent sweep)
其中,初始标记、重新标记仍然需要”Stop the World”,但是它们的速度都很快。初始标记只是标记一下 GC Roots 能直接关联到的对象,速度很快,并发标记阶段就是进行 GC Roots Tracing 的过程,重新标记是为了修正并发标记期间,因用户线程继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始化标记阶段稍长一些,但远比并发标记的时间短。
由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发的执行的。
CMS 的主要优点是并发收集、低停顿,也称之为并发收集低停顿收集器(Concurrent Low Pause Collector),其主要缺点如下: - CMS 收集器对 CPU 资源非常敏感,在并发阶段,它虽然不会导致用户线程停顿,但是它会占用一部分 CPU 资源进行垃圾收集从而导致应用程序变慢,总吞吐量会降低。
- 由于 CMS 并发清除阶段用户线程还在运行,伴随程序的运行必然还有的新的垃圾产生,这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理掉它们,只好留待下一次 GC 时再进行清理。也是由于垃圾收集阶段用户线程还需要运行需要预留足够内存给用户线程使用,如果 CMS 运行期间预留内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,虚拟机只得临时启动 Serial Old 进行老年代垃圾收集,这样会导致长时间停顿
- 由于 CMS 是一款采用标记-清除算法实现的垃圾收集器,收集结束时会有大量的空间碎片产生,空间碎片过多时,如果分配大对象找不到足够大的连续空间分配当前对象,就不得不提前触发一次 Full GC。
3.7 G1 收集器
G1 基于“标记-整理”算法实现,不会产生空间碎片,对于长时间运行的应用系统来说非常重要;另外它可以非常精准地控制停顿,既能让使用者指定一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。
G1 收集器可以实现在基本不牺牲吞吐的前提下完成低停顿的内存回收,这是由于它能够避免全区域的垃圾回收,而 G1 将 Java 堆(包括新生代、老生代)划分成多个大小固定的独立区域,并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(这就是 Garbage First 名称的由来)。