java垃圾回收器--如何回收?

概述
1 说起垃圾回收我们需要想到三个问题

  1. 那些内存需要回收
  2. 什么时候回收
  3. 如何回收

2 这篇博客就来阐述一下java对象中那些内存需要回收,如何判断对象是否死亡。
  首先谈论到内存回收我们首先需要知道java虚拟机的运行时内存的各个部分,其中程序计数器,虚拟机栈,本地方法栈3个区域随线程而生,随线程而灭。虚拟机栈中的栈帧随方法的进入和退出有条不紊的执行这进栈和出栈的操作。每一个栈帧中分配的内存基本上是在类结构确定下来的时候就已知的。因此这几个区域的内存分配和回收基本上都是确定性的,在这几个内存区域就不需要过多考虑回收的问题,因为方法结束或者线程结束时就是内存自然回收的时机。但是java堆和方法区不一样,因为java堆和方法区是线程共享的,java堆中存放着几乎java世界中的所有对象实例。一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建那些对象,这部分内存的分配和回收都是动态的,垃圾收集器主要所关注的就是这部分内存。

3 对象已死么?
垃圾收集器在对堆进行回收之前是如何判断对象是否死亡的呢?

  1. 引用计数算法
    引用计数算法就是给每个对象添加一个引用计数器,每当有一个地方他时,计数器的值就加1,当引用失效的时候计数器的值就减1,当引用计数器为0的时候就代表这个对象是一个可回收的对象。但是主流的java虚拟机中没有选用引用计数算法来管理内存,其中最主要的原因是他不能解决对象之间的循环引用的问题。

例如:
在testGC()方法中,对象objA和objB都有字段instance,赋值令objA.instance=objB及objB.instance=objA,除此之外这两个对象再无任何引用,实际上这两个对象都已经不能再被访问,但是它们因为相互引用着对象方,异常它们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收它们。

/** 
 * 执行后,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 main(String[] args) {  
        ReferenceCountingGC objA = new ReferenceCountingGC();  
        ReferenceCountingGC objB = new ReferenceCountingGC();  
        objA.instance = objB;  
        objB.instance = objA;  

        objA = null;  
        objB = null;  

        //假设在这行发生了GC,objA和ojbB是否被回收  
        System.gc();  
    }  
}  

0.193: [GC 4418K->256K(61504K), 0.0046018 secs]
0.198: [Full GC 256K->160K(61504K), 0.0125962 secs]

在运行结果中可以看到GC日志中包含”4418K->256K”,老年代从4418K(大约4M,其实就是objA与objB)变为了141K,意味着虚拟并没有因为这两个对象相互引用就不回收它们,这也证明虚拟并不是通过通过引用计数算法来判断对象是否存活的。大家可以看到对象进入了老年代,但是大家都知道,对象刚创建的时候是分配在新生代中的,要进入老年代默认年龄要到了15才行,但这里objA与objB却进入了老年代。这是因为Java堆区会动态增长,刚开始时堆区较小,对象进入老年代还有一规则,当Survior空间中同一代的对象大小之和超过Survior空间的一半时,对象将直接进行老年代。

  1. 可达性分析算法 (主流判断对象是否死亡)
    可达性分析算法来判断对象是否死亡,这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(reference chain)。当一个对象到GC Roots没有任何引用链
    相连(用图论来说的话,就是这个对象到GC Roots不可达时),则证明这个对象是不可用的。
    这里写图片描述
    在java语言中可以作为GC Roots的对象包括以下几种
    虚拟机栈(栈中的本地变量表)中引用对的对象。
    方法区中类静态属性引用的对象。
    方法区中常量引用的对象。
    本地方法栈中引用的对象。

那么在可达性分析算法中不可达的对象就是非死不可么?当然不是,这时候不可达对象暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程。如果对象在进行可达性分析算法的时候发现没有与GC Roots相连接的引用链,那他将会被进行第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这另种情况都视为“没有必要执行”。
如果这个对象被判定“有必要执行finalize()方法”,那么这个对象将会被放置在一个叫做F-Queue的队列之中。稍后由一个虚拟机自动创建的,低优先级的finalizer
线程去执行它。这里所谓的执行是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢或者发生了死循环,将有可能导致F-Queue队里中的其他对象处于永久等待中,甚至导致整个内存回收系统崩溃。finalize()方法时对象逃脱死亡命运的最后一次机会。稍后GC将对F-Queue进行第二次的小规模标记。如果对象要在finalize()中成功解救自己只要重新与引用链上的任何一个对象建立关联即可。那么第二次标记时它将会被移除“即将回收”的集合,如果对象这时候还没有逃脱,那基本上就真的被回收了。从代码清单中一个对象的finalize()被执行,但是他任然可以存活。
这里写图片描述
这里写图片描述

从代码清单的运行结果可以看出,SAVE_HOOK对象的finalize()方法确实被GC收集器处罚过。并且在被收集前成功逃脱了。
另外一个值得注意的地方是,代码中有两段完全一样的代码片段,执行结果确实一次逃脱成功,一次失败。这是因为任何一个对象的finalize()方法都只会被系统自动调用一次。如果对象面临下一次回收,他的finalize()方法不会被再次执行,因此第二段代码的自救行动失败了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值