垃圾收集理论

对象已死吗?

引用计数算法

​ 给对象添加一个引用计数器,每当有一个地方引用它时,计数器加1,引用失效计数器减1,任何时刻计数器为0的对象就是不可能再被使用的。

应用场景:

  1. 微软的COM(Component Object Model)技术
  2. ActionScript3FlashPlayer

缺点:

  • 无法解决循环引用。

可达性分析算法

​ 通过一系列GC Roots的对象作为起始点,从这些节点往下搜索,搜索走过的路径称为引用链 (Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象不可用。

GCRootSet

可作为GC Roots对象包括以下几种

  1. 虚拟机栈
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中JNI(native方法)引用的对象

垃圾收集算法

标记-清除算法(Mark-Sweep)

算法分为“标记”和“清除”两个阶段,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,一般采用可达性分析算法。

缺点:

1. 效率低,标记和清除两个过程效率都不高
2. 空间问题,标记清除之后会产生大量不连续的内存碎片,可能会导致无法分配较大对象而触发GC

复制算法

​ 为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,它将可用内存按容量分为大小相等的两块,每次使用一块。当一块内存用完了,就将存活的对象复制到另一块,然后将原来的那块全部回收,也就解决了内存碎片的问题,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

缺点:内存只有一半可用,土豪算法。

​ 不过这个内存比例并不一定是1:1的,现代的商业虚拟机都采用这种算法来回收新生代,IBM公司研究表明,98%的对象都是朝生夕死的。一般新生代被分为一个较大的Eden和两个较小的survivor,一般比例是8:1:1,新生代可用的内存区域是Eden和一块survivor,也就是新生代内存利用率为90%。

我们不能保证每次回收都只有不多于10%的对象存活,此时需要依赖其他内存(老年代)进行担保分配。

标记-整理算法(Mark-Compact)

​ 复制算法在对象存活率高时,复制效率会降低,更关键的是,如果不想浪费50%的空间,就需要有额外的担保空间进行分配担保,以应对被使用额内存中所有对象都100%存活的极端情况,所以老年代一般不能直接使用复制算法。

​ 整理过程是让存活的对象往一端移动,然后将存活端边界以外的内存进行清理。

分代收集算法(Generational Collection)

​ 当代商业虚拟机的垃圾收集都采用“分代收集”算法,这种算法没有引入新的思想,只是根据对象的存活周期将内存划分为几块。

​ 一般将Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

​ 在新生代中,每次垃圾收集都发现有大批对象死去,只有少量存活,那就选用“复制算法”,只需要复制少量的存活对象就可以完成收集。

​ 在老年代中,因为存活率极高,没有额外的空间担保分配,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

Hotspot算法实现

枚举根节点(GC Roots)

​ 从可达性分析中从GC Roots节点查找引用链这个操作为例,可作为GC Roots的节点主要在全局性的引用(常量/类静态属性...)与执行上下文(例如栈帧中的本地变量表)中,现在很多应用仅仅方法区就数百兆,如果要逐个检查这里面的引用,那么必然会消耗很多时间。

​ 另外,可达性分析对执行时间的敏感还体现在GC停顿上,因为分析时必须在一个能确保一致性的快照中进行。这里的“一致性”的意思是指在整个分析过程,整个执行系统看起来就像冻结在某个时间点上,不可用出现对象引用关系还在不断变化的情况,否则分析结果无法保证准确性。

​ 要实现“快照”,必须停顿所有的Java执行线程(SUN称为“Stop The World”)的其中一个重要原因,即使在号称几乎不会发生停顿的CMS收集器中,枚举根节点也是必须停顿的。

​ 由于目前主流的Java虚拟机使用的都是准确式GC,所以当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当有办法直接得知哪些地方存放着对象引用

​ 在Hotspot实现中,使用了OopMap数据结构。在类加载完成时,Hotspot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。

安全点(Safe Point)

​ 在OopMap的协助下,Hotspot可以快速完成GC Roots枚举,但是当引用关系变化,或者说OopMap内容变化的指令非常多,如果为每一条指令都生产对于的OopMap,那将会需要大量的额外空间。

​ GC发生时如何让所有执行线程都运行到最近的安全点上再停顿下来,有两种方案实现:

  1. 抢占式中断

    ​ 首先中断全部线程,如果有线程没到安全点,就恢复线程,让他执行到安全点。

  2. 主动式中断

    ​ 当GC需要中断线程时,不直接对线程操作,仅仅做个标志,各个线程执行时主动轮询这个标志,发现中断标志为真时就把自己挂起。

安全区域(Safe Region)

​ 似乎安全点解决了如何进入GC的问题,但是并不,当某线程不执行的时候,例如处于block/sleep状态时,此时需要安全区域解决。

安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域内的任意地方开始GC都是安全的。也可以将安全区域看成安全点的扩展。

垃圾收集实现

新生代收集器

Serial 收集器

单线程收集器,执行收集时,暂停其他全部线程。

Client模式的虚拟机下的默认新生代收集器。

ParNew 收集器

Serial收集器的多线程版本,依然要停止用户线程,只是新生代多线程收集。

Server模式下首选的新生代收集器,因处理Serial收集器外,唯一一个可以与CMS收集器配合工作。

Parallel Scavenge 收集器

吞吐量优先收集器,可以指定一定内完成收集工作,收集器将尽可能保证完成任务,不过这种速度是利用新生代空间换来的,如果时间调小了,意味着新生代空间变小,意味着可能导致频繁触发GC。

当配置了-XX:UseAdaptiveSizePolicy参数时,就不需要手动配置新生代的分配比例、晋升老年代对象大小等细节参数了,虚拟机会根据系统运行状况收集性能监控信息动态调整这些参数,这种调节方式称为GC的自适应调节策略(GC Ergonomics)。

自适应策略也是其与ParNew收集器重要区别。

老年代收集器

Serial Old 收集器

Serial收集器的老年代版本,采用“标记-整理”算法。

Parallel Old 收集器

Parallel Scavenge的老年代版本,使用多线程和“标记-整理”算法。JDK1.6开始发布,在1.6之前,Parallel Scavenge收集器极其尴尬,除了Serial Old(PS MarkSweep)别无选择。

CMS 收集器 (Concurrent Mark Sweep)

CMS收集器是一种以获取最短回收停顿时间为目标的收集器。

适用于B/S系统的服务器上。

标记过程:

  • 初始标记(CMS initial Mark)
    • 标记GC Roots直接关联到的对象,速度很快,期间Stop The World
  • 并发标记(CMS concurrent Mark)
    • GC Roots Tracing的过程
  • 重新标记(CMS remark)
    • 修正并发标记期间用户继续运作导致标记产生变动的那一部分对象的标记记录。这个阶段会停顿比初始标记停顿稍微长点,但远比并发标记的时间短,期间Stop The world
  • 并发清除(CMS concurrent sweep)

缺点:

  • CMS收集器对CPU资源非常敏感

    • 由于收集线程与用户线程并发,所以收集线程抢占了一部分用户线程的CPU,从而导致用户线程变慢。
  • CMS收集器无法收集浮动垃圾,可能出现Concurrent Mode Failture失败而导致另一次Full GC的产生。

    • 浮动垃圾:用户线程和收集线程并发时,用户线程产生的新垃圾。
    • CMS无法收集浮动垃圾,只能留到下一次GC再清理。
    • CMS需要老年代预留一部分空间给浮动垃圾安家,通过参数-XX:CMSInitiatingOccupancyFraction
    • 如果调太高导致空间不足以浮动垃圾安家,可能会导致GC频繁,反而造成停顿时间增长。
  • 基于标记-清除算法带来的大量内存碎片

    • CMS提供了一个参数-XX:+UseCMSCompactAtFullCollection开关参数(默认开启),用于在CMS收集器顶不住要进行FullGC时开启内存碎片整理。
    • 内存整理过程是无法并发的,但是停顿时间边长了。
    • 虚拟机设计者提供了另一个参数-XX:CMSFullGCCsBeforeCompaction
      • 用于设置执行多少次不压缩的FullGC后,跟着来一次带压缩得到
      • 默认值为0,表示每次进入Full GC都进行碎片整理

G1 收集器

G1收集器是当今收集器最前沿的成果之一,它是面向服务端的垃圾收集器。

特点:

  • 并行与并发
    • 使用多CPU来缩短Stop-The-World停顿时间
    • 部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让JAVA程序继续执行。
  • 分代收集
    • G1不需要其他收集器配合就可以独自完成收集,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
  • 空间整合
    • 与CMS的“标记-清理”不同。G1从整理来看是基于“标记-整理”算法实现的收集器
    • 从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存碎片
  • 可预测的停顿
    • 降低停顿时间是G1和CMS共同的关注点,但G1为了追求低停顿外,还能建立可预测的停顿时间模型,能够让使用者明确指定在一个长度为M毫秒额时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这已经是实时Java的垃圾收集器的特征了。

运作流程:

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

GC日志

-XX:+PrintGCDetails:打开内存回收日志