Java垃圾回收器

垃圾收集器

目录

  • 判定对象存活
    • 引用计数法
    • 可达性分析算法
    • 再谈引用
      • 强引用
      • 软引用
      • 弱引用
      • 虚引用
    • 生存还是死亡
    • 回收方法区
  • 垃圾收集算法
    • 标记-清除算法
    • 复制算法
    • 标记-整理算法
    • 分代收集算法
  • HotSpot的算法实现
    • 枚举根节点
    • 安全点
    • 安全区域
  • 垃圾收集器
    • Serial收集器
    • ParNew收集器
    • Parallel收集器
    • Serial Old 收集器
    • Parallel Old 收集器
    • CMS收集器
    • G1收集器

判定对象的存活

引用计数算法

  • 给一个对象添加一个计数器,每当有一处地方引用对象时,计数器值加1; 当引用失效时, 计数器值就减1; 任何时刻计数器为0对象就是不可能再被使用的.
  • 引用计数算法最大的缺点就是很难解决对象之间的循环引用.

可达性分析算法

  • 通过一系列的称为 “ GC Root “ 对象作为起始点, 从这些节点开始向下搜索, 搜索走过的路径称为引用链, 当一个对象到” GC Root “ 没有任何引用链相连时,则证明此对象是不可用的.

  • 在Java语言中, 可作为 GC Root的对象包括下面几种:
    • 虚拟机栈(栈帧中的本地变量表)引用的对象
    • 方法区中的静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中JNI(一般指Native方法)引用的对象

再谈引用

在JDK1.2后,Java对引用进行了扩充,将引用分强引用, 软引用, 弱引用和虚引用四种.

强引用

程序代码中普遍存在的,类似”Object obj = new Object()”这类引用, 只要强引用存在的话, 对象就永远不会被回收

软引用

软引用用于描述一些还有用但是又是非必需对象. 对于软引用关联着的对象, 在系统将要发生内存溢出之前, 将会把对象列进回收范围之中进行第二次回收, 如果这次回收没有足够的内存,才抛出内存溢出异常

弱引用

弱引用也是用来描述非必需对象, 但是它的强度比软引用弱一些, 被弱引用关联的对象只能生存到下次垃圾收集发生之前.
当垃圾收集器工作时, 无论当前内存是否足够, 都会回收掉只被弱引用关联的对象.

虚引用
  • 它是最弱的一种引用关系.
  • 一个对象是否有虚引用的存在, 完全不会对其生存时间构成影响, 也无法通过虚引用来取得一个对象的实例
  • 一个对象虚引用关联的目的就是这个对象被收集器回收时收到一个系统通知.

生存还是死亡

  • 一个对象需要至少经历两次标记才能宣告死亡
  • 如果一个对象在可达性分析后发现没有GC Roots相连接时, 那么它第一次被标记并且进行一次筛选, 筛选的条件是该对象是否有必要执行finalize()方法.当对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过一次, 这两种情况虚拟机都是视为不必要调用.
  • 如果这个对象被判定为需要执行finalize()方法的话, 那么对象将会被放置在F-Queue队列中, 稍后虚拟机会启动一个优先级较低的线程去执行它.
  • 执行finalize()方法时, 虚拟机并不会等待它执行结束, 这样做是为了避免执行finalize()方法缓慢或者发生死循环. 这样可能会导致F-Queue队列中的对象处于永久等待中,甚至导致整个内存回收崩溃.
  • finalize()方法是对象逃脱死亡命运的最后一次机会, GC会在finalize()中进行第二次小规模的标记, 如果对象被第二次标记后,那么就宣告了一个对象的死亡.

回收方法区

  • 方法区的回收主要回收两部分内容:废弃常量和无用的类.
  • 回收废弃常量和回收Java堆很相似.例如: 字符串 “abc” 已经进入了常量池, 但是当前系统没有一个String对象叫做”abc”的,也就是没有任何String对象引用常量池之中的”abc”, 如果这时候发生内存回收, 而且必要的话, 这个”abc”会被清除出常量池.常量池中的其他类(接口),方法,字段的符号引用也与此类似.
  • 一个类被视为无用的条件
    • 该类的所有实例都已经被回收
    • 加载该类的ClassLoader已经被回收
    • 该类对应的java.lang.Class对象没有在任何地方被引用, 无法通过反射获取类的方法.

垃圾收集算法

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

  • 首先标记出所有需要回收的对象,在完成标记后统一回收所有被标记的对象
  • 不足的地方
    • 效率问题, 标记和清除的两个过程效率都不是很高
    • 空间问题, 标记清除后会产生大量不连续的内存碎片, 空间碎片太多可能会导致以后在程序运行过程中需要分配较大的对象时, 无法找到足够的连续内存而不得不提前触发另外一次GC

复制算法(Copying)

  • 复制算法是为了解决标记-清除算法的效率问题而出现的
  • 它将内存按照容量划分为相等的两块, 每次只用其中一块.当这一块内存快用完了, 就将还存活的对象复制到另一块内存,然后再把空间一次清理掉
  • 复制算法的代价是将内存空间缩小为原来的一半.
  • HotSpot虚拟机默认Eden和Survivor的大小比例为8:1,只有10%的内存空间会被浪费.
  • 如果另外一块Survivor空间没有足够的内存放上次新生代存活下来的对象时, 这些对象将直接通过分配担保直接进入老年代.
  • 使用于对象存活率低的场景, 例如新生代.

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

  • 复制算法在对象存活率高时就要进行较多的复制操作, 效率会变低.

  • 如果不想浪费50%的空间的话, 就需要额外的空间进行担保, 以应对所有对象都存活的极端场景.

  • 标记过程仍然与”标记-清除”算法一样, 但后续步骤不是直接对可回收对象进行清理, 而是让所有存活的对象都向一端移动, 然后直接清理掉边界以外的内存.

  • 该算法适用于老年代.

  • 该算法解决了标记-清除算法的内存碎片问题.

分代收集算法

  • 一般将Java堆划分为新生代和老年代, 这样就可以根据各个年代的特点采用适当的算法
  • 在新生代中,每次GC时,都发现大量对象死去, 只有少量对象存活, 那就选用复制算法.
  • 而老年代中因为对象的存活率高, 没有额外的空间进行担保,就必须使用”标记-清除”或者”标记-整理”.

HotSpot的算法实现

在HotSpot上实现垃圾收集算法, 必须对算法的执行效率必须经过严格的考证, 才能保证虚拟机的执行效率

枚举根节点

  • 利用可达性算法来判定对象的存活, 首先得找出所有的全局引用, 也就是GC-Root.

  • 另外, 可达性分析这项工作必须在一个一致性的快照中进行(引用关系不变), 这样才能够保证分析结果的准确性.

  • 目前主流的Java虚拟机使用的都是标准式的GC, 所以当执行系统停顿下来时, 并不需要一个不漏的检查完所有执行上下文和全局的引用位置, 虚拟机使用一组称为OopMap的数据结构来得知哪些地方存放着对象引用.

安全点

  • HotSpot没有为每条指令都生成OopMap, 它只是在特定的位置记录了”这些信息”,这些位置被称为安全点(Safepoint),即程序并非在所有的地方都能停下来GC, 只有在安全点时才能停下来

  • 安全点的选定基本上是以程序”是否具有让程序长时间执行的特征”为标准选定.”长时间执行”的最明显特征就是指令序列复用,例如方法调用,循环引用,循环跳转,异常跳转等,具有这些功能的指令才会生成Safepoint.

  • 对于Safepoint,需要考虑的另外一个问题是如何让GC发生时停止所有线程时,所有线程都跑到安全点. 这里有两种方案可供选择:抢先式中断和主动式中断.

    • 抢先式中断不需要线程的配合,在发生GC时,首先把所有线程全部中断,如果发现有线程中断在不安全点上, 就恢复线程,让它跑到安全点上. 现在几乎没有虚拟机是这样实现的.

    • 主动式的中断是思想是当GC需要中断线程的时候, 不直接对线程进行操作,仅仅简单的设置一个标志, 各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己挂起.轮询标志的地方和安全点是重合的.

安全区域

  • 安全区域是指在一段代码之中,引用关系不会发生变化. 在这个区域中的任何地方开始GC都是安全的.

  • 线程执行到Safe Region时, 首先标识自己已经进入安全区域, 那样当在这段时间内JVM要发起GC时,就不用管已经标识为安全区域状态的线程. 在线程要离开安全区域时, 它首先要检查系统是否已经完成了根节点枚举, 如果完成了,就继续执行,否则它就要等到收到安全离开的信号为止.

垃圾收集器

Senial 收集器

  • Serial 收集器在进行垃圾回收时,必须暂停其他所有的工作线程,直到它收集结束
  • 下面是Serial/Serial Old收集器的工作过程

  • 优点: 简单高效, 对于限定单CPU的环境来说, Serial收集器由于没有线程交互的开销, 专心做垃圾收集自然可以获得最高的单线程收集效率.
  • 使用场景: 在用户的桌面应用场景中, 分配给虚拟机管理内存不会很大, 停顿时间可控制在10几到100多毫秒, 只要不频繁发生,这点的停顿还是可以接受的. 因此, Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择.

ParNew 收集器

  • ParNew收集器其实就是多线程版本的Serial收集器.

Parallel Scavenge收集器

  • Parallel Scavenge收集器的特点是它关注点与其他的收集器不同, CMS等收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间, 而Parallel Scavenge收集器目的是达到一个可控的吞吐量, 所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值.
  • 停顿时间越短越适合需要与用户交互的程序; 高吞吐量则可以高效率地利用CPU时间, 尽快完成程序运算任务, 主要适合后台运算而不需要太多交互的任务.

Serial Old 收集器

  • Serial Old是Serial收集器的老年代版本, 使用”标记-整理”算法, 这个收集器的主要意义是给Client模式下的虚拟机使用.
  • 如果在Server模式下, 那么它有两大主要用途:
    • 在JDK1.5及之前的版本中与Parallel Scavenge收集器搭配使用.
    • 作为CMS收集器的后备预案,在并发收集发生 Concurrent Mode Failure时使用.

Parallel Old 收集器

  • Parallel Old 是Parallel Scavenge收集器的老年代版本.
  • 在吞吐量以及CPU资源敏感的场合, 都可以优先考虑Parallel Scavenge加Parallel Old收集器.

CMS 收集器(Concurrent Mark Sweep)

  • CMS收集器是一种以获取最短回收停顿时间为目标的收集器.
  • CMS 收集器是基于 “标记-清除”算法实现,整个过程分为4个步骤:
    • 初始化标记:仅仅只是标记一下GC Roots能直接关联到的对象, 速度很快.
    • 并发标记:进行GC Roots Tracing的过程.
    • 重新标记:为了修正并发标记期间因用户程序继续运行导致标记产生变动的那一部分对象标记记录,这个阶段的停顿时间一般会比初始化阶段稍微长一些,但远比并发标记的时间短.
    • 并发清除: 清除标记.

  • CMS 收集器明显的3个缺点:

    • CMS收集器对CPU资源非常敏感,在并发阶段,它虽然不会导致用户线程中断,但是会因为占用了一部分CPU资源而导致应用程序变慢,总吞吐量降低.

    • CMS 收集器无法处理浮动垃圾, 可能出现 “Concurrent Mode Failure”失败而导致另一次Full GC产生.浮动垃圾指的是CMS并发清理阶段用户线程还在运行, 这期间产生的垃圾.CMS无法像其他收集器那样等到老年代几乎完全被填满时才收集, 需要预留一部分的空间提供并发手机时的程序运行使用.要是CMS运行期间预留内存无法满足程序需要, 就会出现一次”Concurrent Mode Failure” 失败. 这时虚拟机启动后备预案: 临时启动Serial Old收集器重新进行老年代垃圾收集, 这样停顿的时间就变长了.

    • 由于CMS是基于”标记-清除”算法实现的, 这意味者收集结束时会有大量空间碎片产生.空间碎片太多会给大对象分配带来麻烦,因为无法找到连续足够大的空间来给大对象分配,这就不得不提前触发一次Full GC 了. 为了解决这个问题, CMS收集器提供了一个 -XX:+UseCMSCompactAtFullCollection开关参数,用户在CMS收集器顶不住要进行FullGC时开启内存碎片的合并过程整理, 内存整理无法并发进行, 因此会导致停顿时间变长. 虚拟机设计者还提供了一个参数-XX:CMSFullGCsBeforeCompaction,这个参数用于设置执行多少次不压缩的Full GC后,就跟着一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理)

G1收集器

G1收集器的使命是在未来替代掉JDK1.5之中发布的CMS收集器,与其他收集器相比, G1具有的特点:

并行与并发

G1能充分利用多CPU,多核环境下的硬件优势, 使用多个CPU来缩短Stop-The-World停顿时间

分代收集

虽然G1可以不需要其他收集器配合就能管理整个GC堆,但他能够采用不同的方式去处理新创建的对象和已经存活一段时间,熬过多次GC的旧对象以获取更好的收集效果.

空间整合

G1整体来看是基于”标记-整理”算法实现的收集器,从局部上来看是基于复制算法实现的.

可预测的停顿

G1除了要求停顿降低外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒.

如果不计算维护Remembered Set操作,G1收集器的运作大致可划分为以下几个步骤:

  • 初始标记:标记一下GC Roots能直接关联到的对象并且修改TAMS的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这个阶段需要停顿线程,但耗时很短.
  • 并发标记:从GC Roots开始对堆中对象进行可达性分析, 找出存活对象,这阶段耗时很长,但可用与用户程序并发运行.
  • 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生百年动的那一部分标记记录,虚拟机将这阶段对象变化记录在线程Remembered Set中, 这阶段需要停顿线程,但可并行执行.
  • 筛选回收:首先对各个Region的回收价值和成本进行排行,根据用户所期望的GC停顿时间来指定回收计划.

参考资料:《深入理解Java虚拟机》

分享到