垃圾回收算法

垃圾回收

引用计数法

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

引用计数器法弊端:无法解决循环引用的问题

可达性分析算法

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

可以作为GC ROOT的有以下几种

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中JNI(即一般说的Native方法)引用的对象

标记-清除算法

如同名字一样,该垃圾回收算法分为两个阶段:首先标记出所有要回收的对象,在标记完成后统一回收被标记的对象,后续的所收集算法都是基于这种思路并对其不足进行改进而得到的

它有两个不足之处

  1. 标记和清清除两个过程的效率都不高
  2. 另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,内存碎片过多可能会导致在程序后续运行过程中分配大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集

复制算法

为了解决内存碎片和效率的问题,它将内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将还存活的对象复制到另一块上面,然后把已使用过的内存空间一次性清理掉。

优势:

  1. 每次都是对整块内存进行回收,不用考虑碎片情况,只需要将指针按顺序分配内存即可,实现简单、运行高效

劣势

  1. 内存缩小为了原来的一半

现在商业虚拟机都采用这种收集算法来回收新生代,因为IBM公司的专项研究表明,新生代中98%的对象都是”朝生夕死”,所以并不需要按照1:1的比例来划分内存空间。

所以新生代内存按照8:2方式被划分为Eden(80%),Survivor 0(10%),Survivor 1(10%)

分配担保

这时候会产生一个问题,如果第一次垃圾回收触发时,Eden存活下来的对象比较多,Survivor 0塞不下了怎么办?

会发生一次分配担保,依赖其他内存(这里指老年代)

分配担保好比去银行借款,如果我们信誉良好,在98%的情况下都能按时偿还,于是银行可能会默认我们下一次也能按时按量进行偿还,只需要有一个担保人能保证我们不能还款时,可以从他的账户扣款,那么银行就认为没有风险。

为什么需要两块Survivor

假设只有一块Survivor会发生什么问题呢?

当发生第一次垃圾回收,Eden区往Survivor 0进行复制的时候,没有什么问题,但是当第二次时,因为原来Survivor中已经存在使用过的空间,再往里塞,会存在内存碎片需要处理,增加了耗时,所以,复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生。

S0和Eden被清空,然后下一轮S0与S1交换角色,如此循环往复。如果对象的复制次数达到16次,该对象就会被送到老年代中

具体参考: http://blog.csdn.net/antony9118/article/details/51425581

标记-整理算法

  1. 复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将变低
  2. 如果不想浪费50%的空间,Survivor就会需要分配担保

如果被使用的内存中对象都100%存活的极端情况,所以在老年代中一般不能使用复制算法

老年代一般使用标记-整理算法,与标记清除算法的区别是,第一次标记后,让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存

分代收集算法

将对象存活周期的不同将内存划分为几块,一般把java堆分为新生代和老年代。这样就可以根据各个代的特点采用最适当的收集算法

  1. 在新生代,每次垃圾收集时都会有大量对象死去,少量存活,就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集
  2. 在老年代,因为对象存活率高,没有额外空间对它进行分配担保,就必须使用标记清除或是标记整理算法来进行回收