您的位置:首页 > 编程语言 > Java开发

深入理解Java虚拟机 -- 读书笔记(2):常用垃圾回收算法

2013-03-03 20:22 302 查看
本系列为《深入理解Java虚拟机 》(周志明著)读书笔记

垃圾回收的核心问题有三个:(1)回收哪些内存 (2)何时回收 (3)如何回收

在Java中,需要回收的内存区域包括堆和方法区。方法区在Hotspot中又被称为“永生代”,主要收集这两方面的内容:废弃常量和无用的类。废弃常量比较容易理解,例如常量区存在“abc”的字符串常量,当系统中没有任何String指向“abc”时,则“abc”可以被回收。无用的类的判断要复杂一些,必须同时满足以下三个条件:

Java堆中不存在该类的任何实例
加载该类的classloader已经被回收
该类对应的java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射访问该方法。

垃圾内存判断算法

引用计数法

引用计数法是最为出名的一种垃圾内存判断算法,在很多教科书中都出现过。其原理是:给对象添加一个引用计数器,每当有其他地方引用它时,计数器就增1;当失去一个引用时,计数器值减1;计数器为0时则说明这个对象可以被回收。

这种算法的优点是简单、高效,缺点是无法处理相互引用的情况。例如下面这段代码,对象A和B都有字段instance,令A.instance = B及B.instance=A,除此之外,这两个对象再无引用,实际上这两个对象都可以被回收,但因为他们相互引用,引用计数始终大于1,因此使用这种方式无法回收此类垃圾内存。
public Class RCGC{
public Object instance = null;
private staic final int _1MB = 1024 * 1024;
public staic void test(){
RCGC A = new RCGC();
RCGC B = new RCGC();
A.instance = B;
B.instance = A;

A = null;
B = null

System.gc()
}
}

根搜索算法

目前主流的程序设计语言中,都是使用根搜索算法来判断对象是否存活。此算法的思路是从一系列“GC Roots”的对象作为起始点,开始向下搜索,搜索走过的路径成为“引用链”,当一个对象和GC Roots之间没有任何一条引用链相连时,则证明此对象不可到达。如下图中的object 5, 6, 7没有到GC Roots的引用链,则可以被回收。



finalize方法

根搜索算法不可到达的对象也不会被马上回收,甚至还可能通过finalize方法“复活”。要宣布一个对象的“死刑”,至少要经过两次标记过程:如果对象中进行根搜索后发现没有与GC Roots相连,则被第一次标记并进行筛选。筛选的过程是检查对象是否需要执行finalize方法,若对象没有覆盖finalize方法或finalize方法已经执行过,则虚拟机将这两种情况视为“没有必要执行”finalize方法。

如果这个对象被JVM判定为需要执行finalize方法,则它会被放入一个F-Quene队列中,并在稍后由一条JVM自动建立的、低优先级的Finalize线程去执行。这里的“执行”是指会触发这个方法,但并不承诺会等待它结束。这么做的原因是:若某个对象的finalize方法执行缓慢或陷入死循环,将可能导致F-Quene队列中对象永久处于等待状态,无法完成回收。而这将导致整个JVM内存回收系统崩溃。

垃圾收集算法

判断出需要回收的垃圾内存后,下一步要做的是确定如何回收内存。下面介绍几种常用的垃圾收集算法的思想:
标记 - 清除算法
从名字可以看出,这种算法分标记和清除两个阶段执行:首先标记出要回收的对象,标记完成后统一回收对象。这种算法的一个明显缺陷是容易产生大量不连续的内存碎片,如下图所示:



复制收集算法

复制算法使用两块内存,当一块内存空间不足时,将还存活的对象复制到另一块内存中,这样就可以避免碎片的问题。复制算法是目前商业虚拟机都在使用的一种方法。因为新生代中对象垃圾收集率很高,因此不需要按1:1的比例来分配内存,而是将内存分为一块儿比较大的Eden空间和两块较小的Survivor空间,每次使用Eden和一块Survivor空间。回收时将Eden和那块Survivor中还活着的对象一次性拷贝到另一块Survivor中,最后清理掉Eden和那块Survivor。HotSpot虚拟机默认Eden和Survivor的大小比例为8:1,这样每次新生代中可用内存空间为整个新生代容量的90%,只有10%的内存空间被“浪费”。当占10%的Survivor空间不能存放GC后的内存怎么办?此时需要依赖其他内存(这里指老年代)进行空间分配担保(Handle
Promotion)。其运行过程如下图所示:

回收前:




回收后:




空间分配担保

在介绍下面的内容之前,首先介绍两个名词:Minor GC和Full GC。
Minor GC:在新生代中进行的垃圾回收。
Full GC:在老年代和新生代中都进行垃圾回收。
一般情况下,Full GC花费的时间远超Minor GC

在理想的复制算法中,应该由两块相等大小的内存来进行相互复制和清理 ,但这样会带来巨大的空间浪费。在新生代,大部分对象的生命期很短,因此Eden和两个Survivor在大多数情况下足够了。为了应对新生代中大量对象Minor GC后仍存活的情况,一般会使用老年代进行空间分配担保。

老年代进行空间分配担保后,Survivor中无法存放的对象将被直接放到老年代中,因此进行担保的前提是老年代本身的剩余空间足以容纳这些对象。因为主担保时JVM无法获知下次GC时剩余对象的大小,所以只能取之前每一次回收晋升到老年代对象容量的平均大小作为经验值,与老年代的剩余空间比较,并决定是否进行Full GC以让老年代腾出更多空间。

使用老年代进行空间分配担保并不能保证万无一失,若某次Minor GC后存活的对象突增,远高于平均值,依然会导致担保失败(Hanlde Promotion Failure)。若出现担保失败,则只好马上发起一次Full GC。

标记 - 整理算法

复制收集算法在对象存活率较高时就要执行更多的复制操作,导致效率降低,所以在老年代中一般不会直接使用复制整理算法。根据老年代对象存活率较高的状况,有人提出了标记 - 清除算法的改进版本:标记 - 整理算法。其标记过程与标记 - 清除算法一样,但后面的步骤不是对直接可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理边界以外的内存,如下图所示:



分代收集算法

当前商业JVM的GC基本都采用“分代收集算法”,即将对象按其生命周期分别存放到不同的内存空间中。Java堆一般分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次收集时对象的存活率不高,适用于复制收集算法,因为只需付出少量的对象复制成本。老年代中因为对象存活率高、没有额外空间对它进行分配担保,使用“标记 - 清理”算法或“标记 - 整理”算法进行回收比较合适。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: