主题:垃圾收集器与内存分配策略

垃圾回收算法

垃圾回收最主要的就属回收Java堆中的对象,此外方法区也会进行回收,主要是回收不在引用的常量以及卸载类,在大量使用反射、动态代理、CGLib等bytecode框架的场景,需要保证虚拟机有回收方法区的能力。判断类可卸载的条件非常严苛,要同时满足:

  • 不存在该类的实例对象
  • 加载该类的ClassLoader已经被回收
  • 对应java.lang.Class对象不存在任何地方被引用,无法通过反射访问该类方法

如何判断对象可回收

引用计数法

给对象添加个计数器,每被引用一次,计数器加1,引用消失,计数器减1。

问题:难以解决循环引用的问题。

根搜索算法

系统中,任何有用的对象,回溯其引用,都可以追溯到几个固定的区域,这些区域引用的对象定义为GC Roots,包括:

  • 虚拟机栈中引用的对象
  • 方法区类静态属性引用的对象
  • 方法区常量引用的对象
  • 本地方法栈JNI引用的对象

从这些GC Roots对象向下搜索,遍历引用链,那些不可达的对象便是不再使用的对象

何时回收

经过跟搜索后,那些不可达的对象,不代表一定会被回收,只是被判了“缓刑”,真正回收前至少要经历两次标记的过程:

  • 搜索后对不可达对象做第一次标记,并进行筛选,筛选出需要执行finalize()方法的对象(条件1.复写了finalize()方法,2.该方法为被虚拟机执行过),并将其放到F-Queue中去
  • 虚拟机Finalizer线程(低优先级)去执行F-Queue中对象finalize()方法(但不保证方法执行完),对象可以在finalize()方法中将自己或其它对象重新与可达对象建立引用
  • 对F-Queue中对象进行第二次标记,移除被救活的对象,而未被救活的对象将被回收

不建议使用finalize()方法,一方面是运行代价高昂(因为可能存在对象复活,所以必须进行两次标记才能回收),另一方面存在很大的不确定性(首先是垃圾回收的时机对用户程序来说一般是不可预测的,其次虚拟机不保证finalize()方法一定执行完,最后一个对象第一次尝试回收和第二次回收finalize()方法的执行也是不一样的)

如何回收

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

先标记要回收的对象,再统一回收标记的对象

  • 标记和清除的效率都不高
  • 内存会存在碎片

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

标记阶段和MS算法一样,接下来将存活对象往一端移动,然后一次性清理边界以外的内存

  • 内存碎片化问题解决了
  • 整理的开销比较大

复制算法

1:1复制

将内存一分为二,对象仅在一份上分配,当这份空间不足时,将有用对象复制到另外一份,同时将当前这份一次性擦除(这个操作开销很小)。如此往复地在两份空间间复制。

  • 算法简单快速,但需要保留一半空闲空间用于复制

Eden + Survivor分配

IBM统计数据,98%的对象是“朝生夕灭”的,因此没必要保留一半空间

  • -XX:SurvivorRatio
  • 分配担保机制

分代回收算法

根据对象存活周期不同将内存分块,不同的块采用不同的回收算法

通常分为新生代和年老带

内存分配与回收策略

垃圾收集器

  1. Serial 收集器
    • 单线程新生代收集器
    • 需要暂停用户线程
  2. Serial Old 收集器
    • Serial收集器的年老代版本
    • 需要暂停用户线程
  3. ParNew 收集器
    • Serial多线程版(新生代)
    • 需要暂停用户线程
  4. Parallel Scavenge 收集器
  5. Parallel Old 收集器
  6. CMS 收集器
  7. G1 收集器


blog comments powered by Disqus

Published

17 April 2014

Tags