虚拟机是如何判断一个对象是否需要回收

虚拟机是如何判断一个对象是否需要回收


        我们常说的垃圾回收,主要指的是Java堆方法区的垃圾回收。一个接口的多个实现类需要的内存可能不一样,而编译期只知道对象的静态类型;一个方法中需要创建多少对象,也只有在运行期才知道,因此,这些部分的内存分配和回收都是动态的,垃圾收集器关注的是这部分的内存。

故而这里讨论的内存分配和回收,也仅是针对Java堆和方法区的内存。


对象生死的判断策略

垃圾收集器在对堆进行回收之前,第一件事就是要确定哪些对象已经“死去”,需要回收。判断对象生死的算法,主要有以下两种。


引用计数算法

这种算法,给每个对象设置一个引用计数器,每当有一个地方引用它时,计数器加1;引用失效时,计数器减1;计数器为0,意味着对象独自漂泊在堆中,没人认识它,不可能再被使用,这时就是一个“废柴”,可以回收了。

这种算法,实现简单,判定效率也高,但是有一个致命的缺陷——很难解决对象之间相互引用的问题。

什么是对象相互引用,看下面这个例子:

[java]  view plain  copy
  1. /** 
  2.  * testGC()方法执行后,objA和objB会不会被GC呢?  
  3.  */  
  4. public class ReferenceCountingGC {  
  5.   
  6.     public Object instance = null;  
  7.   
  8.     private static final int _1MB = 1024 * 1024;  
  9.   
  10.     /** 
  11.      * 这个成员属性的唯一意义就是占点内存,以便在能在GC日志中看清楚是否有回收过 
  12.      */  
  13.     private byte[] bigSize = new byte[2 * _1MB];  
  14.   
  15.     public static void testGC() {  
  16.         ReferenceCountingGC objA = new ReferenceCountingGC();  
  17.         ReferenceCountingGC objB = new ReferenceCountingGC();  
  18.         objA.instance = objB;  
  19.         objB.instance = objA;  
  20.   
  21.         objA = null;  
  22.         objB = null;  
  23.   
  24.         // 假设在这行发生GC,objA和objB是否能被回收?  
  25.         System.gc();  
  26.     }  
  27. }  

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不可达的对象;

这里以弱引用为例,演示一下如何使用引用类型:

[java]  view plain  copy
  1. public class WeakReferenceTest {  
  2.     public static void main(String[] args) throws InterruptedException {  
  3.         WeakReference<WeakReferenceTest> weakReference = new WeakReference<WeakReferenceTest>(new WeakReferenceTest());  
  4.         // 第一次打印弱引用所引用的对象   
  5.         System.out.println(weakReference.get());  
  6.         // 进行一次GC  
  7.         System.gc();  
  8.         // 由于GC进行需要时间,这里等一秒钟  
  9.         Thread.sleep(1000);  
  10.         // 再次打印弱引用所引用的对象   
  11.         System.out.println(weakReference.get());  
  12.     }  
  13. }  

运行结果:

com.hzy.jvm.chp03.WeakReferenceTest@6f92c766
null


起死回生

即使在可达性分析算法中不可达的对象,也不是“非死不可”的。

对象在被标记为不可达之后,如果对象覆盖了finalize()方法并且该对象还没有调用过finalize(),那么这个对象会被放入F-Queue队列中,并在稍后一个由虚拟机建立的、低优先级Finalize线程中去执行对象的finalize()方法。稍后GC会对F-Queue的对象进行再一次的标记,如果对象的finalize方法中,将对象重新和GC Roots建立了关联,那么在第二次标记中就会被移除出“即将回收”的集合。

但是,finalize线程的优先级很低,GC并不保证会等待对象执行完finalize方法之后再去回收,因而想通过finalize方法区拯救对象的做法,并不靠谱。鉴于finalize()方法这种执行的不确定性,大家其实可以忘记finalize方法在Java中的存在了,无论什么时候,都不要使用finalize方法。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值