垃圾收集器与内存分配策略

2017-04-23  本文已影响23人  大海孤了岛

3.1 概述

虽然目前内存的动态分配和内存回收技术已经相当成熟,一切看起来都进入了“自动化”时代。但当需要排查各种内存溢出,内存泄漏问题时,以及垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要去对这些“自动化”技术进行必要的监控和调节了。

我们知道Java内存运行时区域分为方法区,堆,虚拟机栈,本地方法栈和程序计数器五部分。其中虚拟机栈,本地方法栈和程序计数器这3个区域随线程而生,随线程而灭,因此对这些区域的内存分配和回收都是确定的。而Java堆和方法区就不同,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也不一样,我们只有在程序运行期间才能知道会创建哪些对象,这部分的内存和回收都是动态的。因此,垃圾收集器所关注的部分在Java堆和方法区。

3.2 对象已死吗

在Java堆中存放着几乎所有的对象实例,垃圾收集器在堆中进行回收前,需要确定哪些对象是“存活”着,哪些对象是已经”死去“的。

判断对象是否存活的两种算法

给对象中添加一个引用计数器,每当一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

评价:引用计数算法简单高效,但它存在一个致命的问题:很难解决对象之间相互引用的问题。

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;
        //设置为null
        objA = null;
        objB = null;

        System.gc();
    }

}

如上,ObjectA和ObjectB之间相互引用着,导致两个对象的引用计数值都不为0,因此,无法通知GC收集器来回收它们。

通过一系列的”GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为“引用链”。当一个对象到GC Roots没有任何引用链相连时,则证明该对象是不可用的。

Paste_Image.png

如上图,其中Object1-4是不可回收的,而Object5-7是可回收的,因为它们到GC Root是不可达的。

GC Root的对象:

3.2.3 再谈引用

在传统意义上,如果reference类型的数据中存储的数值代表是另一块内存的起始地址,就称为这块内存代表着一个引用。但在实际应用中,我们希望能描述这样一类对象:当内存空间足够时,则能保留在内存之中;如果内存空间在进行垃圾收集之后还是非常紧张,则可以抛弃这类对象。

引用的扩充

3.2.4 对象的死亡

若一个对象在可达性算法中为不可达对象,该对象也并非被直接宣布”死亡“,而只是处于”缓刑“阶段。因为要真正宣告一个对象的死亡,至少要经历两次标记过程。

public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;

    public void isAlive(){
        System.out.println("yes, i'm still alive");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
        //与GC Root引用链进行关联
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new FinalizeEscapeGC();
        
        //对象进行第一次自救
        SAVE_HOOK = null;
        System.gc();
        //因为finalizer方法优先级很低,因此暂停一会等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i'm dead.");
        }

        //进行第二次自救
        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(500);
        if (SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i'm dead.");
        }
    }
}

输出结果:
finalize method executed!
yes, i'm still alive
no, i'm dead.

我们可以看到同样的自救方法,第一次是成功的,而第二次却失败了。这是因为任何一个对象的finalizer方法只会被系统自动调用一次

3.2.5 回收方法区

方法区(永久代)的垃圾收集主要回收废弃常量和无用的类。

判断一个常量是否废弃是非常简单的事,而对于判断一个类是否为“无用的类”的条件则相对苛刻,必须要满足以下三个条件:

3.3 垃圾收集算法

3.3.1 标记-清除算法

首先标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

标记-清除.png
3.3.2 复制算法

它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另一块上,然后再把已使用的内存空间一次性清理掉。

复制算法.png
3.3.3 标记-整理算法

复制收集算法在对象存活率较高时就需要进行较多的复制操作,效率将会变低。因此,在老年代一般不会选用复制算法。根据老年代的特点,提出了”标记-整理“算法,标记过程和”标记-清除“算法一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理端边界以外的内存。

标记-整理.png
3.3.4 分代收集算法

一般将Java堆分为新生代和老年代。在新生代中,每次垃圾收集都会有大量的对象死去,只有少量的存活,因此选用复制算法。在老年代中,对象存活率高,没有额外空间对它进行分配担保,就必须使用”标记-清除“和”标记-整理“算法。

上一篇 下一篇

猜你喜欢

热点阅读