Java虚拟机(JVM) 之 GC

2019-03-20  本文已影响0人  波波维奇c

其实我以前看到提到 JVM 之类的东西,就觉得这玩意太难,看不下去啊。可是要进阶吧,又是必须的啦,所以说一句 干就完了


image

今天就来学学 GC,想问就我一个人想到中文的滚粗吗


image

其实这么理解也可以啦,GC 是什么意思呢,可以大概这么理解 公厕只有这么些个,偏偏有些人占着茅厕不那啥,只好让管理员让它滚粗啦,好让其它人可以用,让世界和平。

好了好了 回来了回来了,为什么会有 GC (回收垃圾,释放内存)呢?因为内存就这么大,用完了不回收,就一直占用着,可以用的内存就越来越小,到最后溢出了(OOM),程序也就GG了。

那么问题来了,怎么知道这个占用着的对象是不是垃圾呢?

一:引用计数法

给对象添加一个引用计数器,每当有一个地方引用它时,计数器就加1,当引用失效时,计数器就减 1,任何时刻计数器为 0 的对象就是不可能再被使用的,就是垃圾了,可以回收了。(但是主流的虚拟机并不是用这种方法)

二:可达性分析算法

讲解这个之前,我觉得还是要先知道 Java 运行时的内存区域是怎么区分的。自行百度?哈哈
网上找了张图:


image

什么意思呢?
意思就是,这个算法呢就是通过一系列称为 “GC Roots” 的对象作为起点,从这些起点向下搜索,搜索走过的路径称为 引用链,当一个对象到 GC Roots 没有引用链的时候,我们就可以指着它的鼻子骂“垃圾”,等着被回收吧?

那么就有人懵逼了 GC Roots 是什么玩意啊,我不知道啊

举个我认为的例子:看过战狼2的都知道,有了中国的护照,我们就是祖国花朵(哈哈),到哪都是中国人的身份,那么国家就是我们最强的后盾。那么我们就可以把 中国理解为 GC Roots,护照就是筛选是否是中国人的 引用链,有就有中国做我们的后盾,没有就等着被回收(没被的意思....)

那么在程序中 什么可以作为 GC Roots呢?
什么时候回收

不同的虚拟机实现有着不同的 GC 实现机制,但是一般情况下每一种 GC 实现都会在以下两种情况下触发垃圾回收。
Allocation Failure:在堆内存中分配时,如果因为可用剩余空间不足导致对象内存分配失败,这时系统会触发一次 GC。
System.gc():在应用层,Java 开发工程师可以主动调用此 API 来请求一次 GC。

那么这里就引出了 引用 的概念,全部有4种引用

注意:即使在可达性分析算法中不可达的对象,也不是 非死不可的,这时候它们暂时处于“缓刑”,真正死亡,至少要经历两次标记的过程:判断对象是否有必要执行finalize()方法;若被判定为有必要执行finalize()方法,之后还会对对象再进行一次筛选,如果对象能在finalize()中重新与引用链上的任何一个对象建立关联,将被移除出“即将回收”的集合。任何对象的 finalize()方法,只会执行一次。

public class FinalizeEscapeGC {

    public static FinalizeEscapeGC SAVE_HOOK = null;

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

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize mehtod executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws Throwable {
        SAVE_HOOK = new FinalizeEscapeGC();

        //对象第一次成功拯救自己
        SAVE_HOOK = null;
        System.gc();
        // 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead :(");
        }

        // 下面这段代码与上面的完全相同,但是这次自救却失败了
        SAVE_HOOK = null;
        System.gc();
        // 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead :(");
        }
    }
}
#################
运行结果:
finalize mehtod execute!
yes, i am still alive :)
no, i am dead :(
两段代码是一样的,执行结果一次成功,一次失败,这就是上面提到的任何对象的 finalize()方法,只会执行一次。

那么问题来了,知道是垃圾后,要怎么去回收呢?当然就是每个人都会说的垃圾收集算法啦

顾名思义,是垃圾 就 标记,然后把标记的给 清除 就好了(标记过程就是上面讲的)
缺点:效率,空间问题。标记清除后会产生大量的不连续内存碎片。

为啥会有这样的缺点,不理解?举个例子:

你种了好多大白菜,一眼看过去,好多个点都有坏死的,而且都不是同一个区域,你依次走过去,标记是要扔掉的,然后就去清理了,这样下来效率就很慢了,而且你拔掉的地方那么小,翻土重新种又太小,容易弄坏其他的白菜,只能空着,那么就是空间问题了。

复制算法

把可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用尽后,把还存活着的对象 复制 到另外一块上面,再将这一块内存空间一次清理掉。
例子:这个就更容易理解了,叫了两打啤酒,每次喝都随机两打里面拿,要结账的时候,把空瓶装一箱,没喝存起来的装一箱,结账时候清理空瓶的那一箱就好了。
缺点:为了解决效率问题。代价 内存缩小了一半

标记 — 整理算法

首先 标记 出所有需要回收的对象,然后进行 整理,使得存活的对象都向一端移动,最后直接清理掉端边界以外的内存。
例子 :平常我们自己整理房间就是啦,杂乱的房间中 不想要的扔掉,不要让它继续堆放在房间占位置,把空出来的存放自己想要的东西,
优点:即没有浪费50%的空间,又不存在空间碎片问题,性价比较高。
一般情况下,老年代会选择标记-整理算法。

分代收集算法

根据对象存活周期的不同将内存划分为几块。一般是把 JAVA 堆分为新生代和老年代,然后根据各个年代的特点采用最适当的收集算法。
新生代:回收时,有大批对象死去,只有少量存活,适合复制算法。
老年代:存活率高,没有额外的分配担保空间,必须使用 标记 — 清理,或 标记 — 整理。

上一篇下一篇

猜你喜欢

热点阅读