垃圾收集算法-标记清除算法
标记清除算法是最基础的收集算法。算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记过程完成后统一回收所有被标记的对象。后续的收集算法都是基于这种思路对其不足进行改善。
主要有两个不足点:
- 一个是效率,标记和清除两个过程的效率都不高;
- 另一个是空间问题,标记清除后会产生大量不连续的内存碎片,碎片太多会导致以后在程序运行过程中需要分配较大的对象时,无法找到连续内存而不得不触发另一次垃圾收集行为。
垃圾收集执行过程:
垃圾收集算法-复制算法
为了解决效率问题有了复制算法,它将可用的内存分为大小相等的两块,每次只使用其中一块。当这一块用完了,就将还存活的的对象复制到另一块上面,然后把已使用过的内存空间一次性清理掉。不用考虑内存碎片等复杂情况。
现代商业虚拟机都用这种算法来回收新生代。因为新生代中98%的对象都是“朝生夕死”,所以不需要按照1:1来分配内存,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用一块Eden和一块Survivor。Eden和Survivor的大小比例是8:1,只有10%的内存会被”浪费“掉,当回收之后有大于10%的对象存活,Survivor空间不够用时,会通过内存担保机制进入老年代。
缺点:
在存活率较高的时候需要进行较多的复制操作,效率将会变低。不适合用在老年代
tip:Java的堆内存分为老年代和新生代(后面会有详细介绍)。
垃圾收集算法-标记整理算法
标记整理算法主要是用在存活率较高的老年代,标记过程与“标记清除算法”一样,但是后续是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。如图:
分代收集算法
分代收集不是一种新算法,只是根据对象存活周期不同将内存划分为几块。一般是新生代和老年代。这样就可以根据每个年代的特点采用最适当的收集算法。新生代中,对象存活率不高,可以选复制算法。老年代存活率高,可以采用“标记清理”或者“标记整理”算法。
HotSpot算法实现-枚举根节点
枚举根节点是用来做可达性分析的,判断哪些对象还活着。因为这项分析工作必须要在一个能确保一致性的快照中进行,不可以出现分析过程中对象引用关系还在不管变化,这点导致GC进行时必须停顿所有Java执行线程(Sun称这件事情为“Stop The World”)。
目前主流Java虚拟机都是准确式GC,所以当系统停顿下来后,并不需要一个不漏的检查所有执行上下文和全局的引用位置,虚拟机使用一组称为OopMap的数据结构来存放这些对象引用的位置。
安全点(Safepoint)
有了OopMap可以快速而准确的完成GC Roots枚举,OopMap内容变化的指令非常多,如果为每一条指令都声称对应的OopMap,那将会需要大量的额外空间。
所以虚拟机只在“安全点”记录这些信息,只有到达安全点才能暂停下来开始GC。安全点不能太少会让GC等待时间太长,也不能太多会增大运行时负荷。所以安全点的选定是以“是否具有让程序长时间执行的特征”来选定。比如:方法调用、循环跳转等,具有这些指令才会产生安全点。
另一个问题时如何在GC时让所有的线程“跑”到最近的安全点上停顿,有两种方案:抢先式中断和主动式中断。
抢先式中断:GC发生时,先中断所有线程,如果线程不在安全点,再恢复线程,让它跑到“安全点”,几乎没有虚拟机这么做。
主动式中断:当GC需要中断线程时,仅仅设置一个标志,各个线程执行时去轮询这个标志,发现中断标志为真时就中断挂起,轮询的标志地方和安全点是重合的。
安全区域
Safepoint机制保证了程序执行时,在不太长的时间就会遇到可进入GC的Safepoint。如果程序“不执行”的时候比如处于Sleep和blocked状态,程序就没有分配cpu执行时间,这时候线程就无法响应JVM的中断请求。这时候就需要安全区域(Safe Region)来解决。
安全区域是指在一段代码中,引用关系不会发生变化。这个区域的任意地方开始GC都是安全的。
线程执行到安全区域的代码时,首先标示自己已经进入了安全区域,当jvm发起GC时就不用管安全区域的线程了。
垃圾收集器
收集算法是内存回收的方法论,垃圾收集器是内存回收的具体实现。基于JDK1.7的所有垃圾收集器如图:
并没有万能的收集器,选择的只是对具体应用最适合的收集器。
Serial收集器
Serial收集器是是最基本、历史最悠久的收集器。这个收集器是单线程的,它在进行垃圾收集时,必须暂停掉所有其他的工作线程,直到它收集结束。运行过程如图:
优点在于它简单而高效,由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程手机效率。在用户桌面场景中,分配给虚拟机管理的内存不会很大,所以停顿时间很短,只要不是频繁发生,这点停顿完全可以接受,所以Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择。
ParNew 收集器
ParNew其实就是Serial收集器的多线程版本。如图是ParNew收集器的工作过程:
它是许多运行在server模式下的虚拟机首选的新生代收集器,一个很重要的原因是,除了Serial收集器外,目前只有它能与CMS收集器配合工作。
可以使用-XX:+UseParNewGC选项来强制指定它。
注意:
后面提到的并发和并行的收集器,可以解释如下:
- 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
- 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。
Parallel Scanvenge 收集器
Parallel Scanvenge收集器是一个新生代收集器,也是采用复制算法的收集器,又是并行的多线程收集器。
Parallel Scanvenge收集器的特点在于他的目标是达到一个控制的吞吐量。吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。
停顿时间越短就越适合需要与用户交互的程序。它提供了两个参数用于精准控制吞吐量。分别是控制最大垃圾收集时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。GC停顿时间是以牺牲吞吐量和新生代空间来换取的。
Serial Old 收集器
Serial Old 收集器是Serial收集器的老年代版本,同样是一个单线程收集器,使用标记整理算法。这个收集器的主要意义也是在于给Cient模式下的虚拟机使用。在Server模式下有两种用途:一种是在JDK1.5及之前的版本中与Parallel Scanvenge收集器搭配使用。另一种用途是作为CMS收集器的后备预案,在并发收集器发生Concurrent Mode Failure时使用。
Parallel Old收集器
Parallel Old是Parallel Scanvenge收集器的老年代版本,使用多线程和“标记-整理”算法。
在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scanvenge加Parallel Old收集器。
CMS 收集器
CMS(Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。适合互联网站或者B/S系统的服务端上。
CMS收集器是基于“标记-清楚”算法实现的。收集过程包括四个步骤:
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)
- 并发清除(CMS concurrent sweep)
其中初始标记、重新标记仍然需要“Stop The World”,初始标记仅仅只是标记GC Roots能直接关联到的对象,很快。并发标记就是进行GC Roots Tracing的过程。重新标记则是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段比初始标记稍长,远比并发标记短。运行示意图如下:
CMS收集器有三个明显的缺点
- CMS收集器对CPU资源非常敏感。
- CMS收集器无法处理浮动垃圾,可能会出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行还有垃圾不断产生,这一部分来记出现在标记过程之后,CMS无法在当次收集中处理掉他们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。
- 由于CMS收集器基于“标记-清除”算法实现,垃圾收集结束会有大量空间碎片产生。
G1 收集器
G1是一款面向服务端应用的垃圾收集器,未来可以替换掉CMS收集器。与其他收集器相比,特点如下:
- 并行与并发: G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World时间。
- 分代收集,分代概念仍然保留。不需要其他收集器配合就能独立管理整个GC堆,采取不同的策略。
- 空间整合:G1从整体来看是基于“标记-整理”算法实现的,从局部来看是基于“复制”算法的,不会产生内存空间碎片。
- 可预测的停顿,能让使用这明确指定在一个长度为M毫秒的时间片段内。
G1收集器的运行示意图: