本系列为《深入理解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。