虚拟机是如何判断一个对象是否需要回收
虚拟机是如何判断一个对象是否需要回收
我们常说的垃圾回收,主要指的是Java堆和方法区的垃圾回收。一个接口的多个实现类需要的内存可能不一样,而编译期只知道对象的静态类型;一个方法中需要创建多少对象,也只有在运行期才知道,因此,这些部分的内存分配和回收都是动态的,垃圾收集器关注的是这部分的内存。
故而这里讨论的内存分配和回收,也仅是针对Java堆和方法区的内存。
对象生死的判断策略
垃圾收集器在对堆进行回收之前,第一件事就是要确定哪些对象已经“死去”,需要回收。判断对象生死的算法,主要有以下两种。
引用计数算法
这种算法,给每个对象设置一个引用计数器,每当有一个地方引用它时,计数器加1;引用失效时,计数器减1;计数器为0,意味着对象独自漂泊在堆中,没人认识它,不可能再被使用,这时就是一个“废柴”,可以回收了。
这种算法,实现简单,判定效率也高,但是有一个致命的缺陷——很难解决对象之间相互引用的问题。
什么是对象相互引用,看下面这个例子:
- /**
- * testGC()方法执行后,objA和objB会不会被GC呢?
- */
- public class ReferenceCountingGC {
- public Object instance = null;
- private static final int _1MB = 1024 * 1024;
- /**
- * 这个成员属性的唯一意义就是占点内存,以便在能在GC日志中看清楚是否有回收过
- */
- private byte[] bigSize = new byte[2 * _1MB];
- public static void testGC() {
- ReferenceCountingGC objA = new ReferenceCountingGC();
- ReferenceCountingGC objB = new ReferenceCountingGC();
- objA.instance = objB;
- objB.instance = objA;
- objA = null;
- objB = null;
- // 假设在这行发生GC,objA和objB是否能被回收?
- System.gc();
- }
- }
testGC()方法的前四行执行之后,objA对象被objA和objB.instance引用着,objB也类似;执行objA=null和objB=null之后,objA对象的objA引用失效,但是objB.instance引用仍然存在,因此如果采用单纯的引用计数法,objA并不会被回收,除非在执行objB=null时,遍历objB对象的属性,将里面的引用全部置为无效。
可达性分析算法
在主流的商业程序语言(Java、C#),都是通过可达性分析来判断对象是否存活的。这个算法的基本思路是:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,当GC Roots到一个对象不可达时,则证明这个对象是不可用的,可以将其回收。
这个算法很好的解决了引用计数法在处理相互引用时遇到的难题,如下图,object5和object6虽然相互引用,但是由于他们到GC Roots都不可达,因此会被判定为可回收的对象。
在Java中,可作为GC Roots的对象主要有两种:
- 全局性的对象,如常量或者类的静态属性,如果一个对象被全局对象所引用,那就不能被回收;
- 执行上下文,如栈帧中的局部变量,如果方法上下文中有局部变量引用了这个对象,那就不能被回收;
虚拟机栈(JVM stack)中引用的对象(准确的说是虚拟机栈中的栈帧(frames))
我们知道,每个方法执行的时候,jvm都会创建一个相应的栈帧(栈帧中包括操作数栈、局部变量表、运行时常量池的引用),栈帧中包含这在方法内部使用的所有对象的引用(当然还有其他的基本类型数据),当方法执行完后,该栈帧会从虚拟机栈中弹出,这样一来,临时创建的对象的引用也就不存在了,或者说没有任何gc roots指向这些临时对象,这些对象在下一次GC时便会被回收掉方法区中类静态属性引用的对象
静态属性是该类型(class)的属性,不单独属于任何实例,因此该属性自然会作为gc roots。只要这个class存在,该引用指向的对象也会一直存在。class 也是会被回收的,在面后说明本地方法栈(Native Stack)引用的对象
可达不一定就安全了
默认情况下,到GC Roots可达的对象都不会被回收,这种对象,我们成为“强引用”。
然而,实际开发中,并不是所有强引用的对象,我们都认为是不能回收的,比如一个从缓存获取的很占用内存的对象,我希望他可以在下一次垃圾收集时被回收,如果下一次需要使用,再从缓存重新获取。
针对这种“食之无味,弃之可惜”的对象,从JDK 1.2开始,Java对引用的概念进行了扩充,将引用分为强引用(StrongReference)、软引用(SoftReference)、弱引用(WeakReference)和虚引用(PhantomReference)4种:
- 强引用:也就是默认的引用,只要到GC Roots可达,就不会被回收;
- 软引用:对象在将要发生内存溢出之前,会被回收;
- 弱引用:对象在下一次GC时被回收;
- 虚引用:形同虚设,虚引用的对象,可以视为GC Roots不可达的对象;
这里以弱引用为例,演示一下如何使用引用类型:
- public class WeakReferenceTest {
- public static void main(String[] args) throws InterruptedException {
- WeakReference<WeakReferenceTest> weakReference = new WeakReference<WeakReferenceTest>(new WeakReferenceTest());
- // 第一次打印弱引用所引用的对象
- System.out.println(weakReference.get());
- // 进行一次GC
- System.gc();
- // 由于GC进行需要时间,这里等一秒钟
- Thread.sleep(1000);
- // 再次打印弱引用所引用的对象
- System.out.println(weakReference.get());
- }
- }
运行结果:
起死回生
即使在可达性分析算法中不可达的对象,也不是“非死不可”的。
对象在被标记为不可达之后,如果对象覆盖了finalize()方法并且该对象还没有调用过finalize(),那么这个对象会被放入F-Queue队列中,并在稍后一个由虚拟机建立的、低优先级的Finalize线程中去执行对象的finalize()方法。稍后GC会对F-Queue的对象进行再一次的标记,如果对象的finalize方法中,将对象重新和GC Roots建立了关联,那么在第二次标记中就会被移除出“即将回收”的集合。
但是,finalize线程的优先级很低,GC并不保证会等待对象执行完finalize方法之后再去回收,因而想通过finalize方法区拯救对象的做法,并不靠谱。鉴于finalize()方法这种执行的不确定性,大家其实可以忘记finalize方法在Java中的存在了,无论什么时候,都不要使用finalize方法。