JVM垃圾回收之我还活着吗

1.序言

在Java堆的世界里,存放着几乎所有的对象实例,所以垃圾回收的主要对象就是堆。而进行回收前,第一件事就是要确定这些对象那些还活着,哪些已经“死去”。而能做这件事的判官主要有两种,一种是引用计数算法,另外一种就是可达性分析算法,当然,判断在判定生死的时候还会给一张仅仅能使用一次的免死金牌(这个待会会说)。接下来让我带大家进入两种算法的真面目,接下来看一下那张免死金牌究竟张啥样。

2.引用计数算法

2.1 概念

对象出生之际都会附带一个引用计数器,每当一个地方对他引用,计数器就会+1;当引用失去,计数器就会-1;任何时刻计数器为0的状态,他就不可能被引用。此时垃圾回收期就会把计数器为0的对象回收。

2.2 特点

1)优点: 处理简单,判定效率高。

2)缺点: 不能解决循环引用的问题。下面提供一段循环引用的代码,大家可以琢磨一下。

public class Box{
    private static final int size = 1024 * 1024;
    private byte[] bigSize = new Byte[2 * size];
    private Box instance;

    public static void testGC(){
        Box redBox = new Box();
        Box blueBox = new Box();

        redBox.instance = blueBox;
        blueBox.instance = redBox;

        redBox = null;
        blueBox = null;

        System.gc();
    }
}

2.3 应用案例

微软公司的Component Object Model技术、ActionScript3的FlashPlayer、Pyhton语言、Squirrel。看到没有,至少Java就没有使用这种算法来管理内存。

3.可达性分析

3.1 准备工作

在进行分析可达性分析之前,我感觉有必要先简单普及一下对象内存布局的三块区域,对象头(Header)、实例数据(Instance Data)和对齐填充。

3.1.1对象头

对象头有32位组成,前25bit存放对象哈希码,4bit用于存放对象分代年龄,2bit用于存放锁标志位,1bit固定为0.

存储内容 标志位 状态
对象哈希码、对象分代年龄 01 未锁定
指向锁记录的指针 00 轻量级锁定
指向重量级所的指针 10 膨胀(重量级锁定)
空,不需要记录信息 11 GC标记
偏向线程ID、偏向时间戳、对象分代年龄 01 可偏向

3.1.2实例数据

这部分真正存放对象实例有效数据,包括父类继承下来的和子类中定义的。这部分的存储顺序会受到虚拟机分配策略和字段在Java源码定位的顺序决定的。

3.1.3 对齐填充

通过名字就可以看出来这一部分只是填充使用,并没有实际作用。

3.1 基本概念

通过一系列称之为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径成为引用链(Reference Chain),当一个对象到GCRoots没有任何引用链的时候,则证明此对象不可用。收集器对于引用链上的对象会在对象头做标记,正如上面对象头分布所示。下图中,object4-object6没有到GCRoots相连,也就是被回收的对象。

3.2 作为GCRoots的对象

3.1 虚拟机栈(栈帧中的本地变量表)中引用的对象。

3.2 方法区中类静态属性引用的对象。

3.3 方法区中常量引用的对象。

3.4 本地方法栈中JNI(即一般说的Native方法)引用的对象。

3.3 引用类型

在Jdk1.2后,Java对引用的概念进行了扩充,分为四种方式引用。

类型 特点 触发时机
强引用(Strong Reference) 引用还在,就一直活着 不会触发(初非不可达)
软引用(Soft Reference) 一些有用但是非必须的对象 当系统要发生内存溢出之前,会对软引用进行二次回收,如果内存还不够,才会发生内存溢出。
弱引用(Weak Reference) 非必须对象 不管他处于何种状况,主要发生GC,他就会被清除。
虚引用(Phantom Reference) 成为幽灵引用或幻影引用 一个对象是否虚引用,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。他的目的是能在这个对象呗收集器回收时收到一个系统通知。

4.免死金牌

要宣布一个对象死亡至少要经历两次标记过程。如果对象在进行可达性分析后发现没有GCRoots相连接,他会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有重写此方法或者已经被调用过,那么虚拟机将这两种情况都视为没有必要执行。如果有必要执行,那么这个对象会放置在一个叫做F-Queue列队中,并稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行他。可以看一下下面代码。

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!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args)throws Throwable{
        SAVE_HOOK = null;
        System.gc();

        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

5 回收方法区

5.1 永久代在垃圾收集主要回收两部分内容:废弃常量和无用类。

5.2 判断一个类是否无用的条件

1)该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。

2)加载该类的ClassLoader已经被回收。

3)该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

5.3使用场景

在大量使用反射、动态代理、CGLib等框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备卸载类的功能,以保证永久代不会被溢出。

发布了72 篇原创文章 · 获赞 24 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/oYinHeZhiGuang/article/details/102710815
今日推荐