JVM探秘(五)-GC,如何判定对象可回收?

一、概述

可达性分析算法,是Java虚拟机判定对象是否可回收的常用算法,它的思路是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。如图所示:
在这里插入图片描述

图中蓝色,代表以GC Root为根节点,路径是可达的,所以在判定时,认为蓝色是不可回收对象。图中橙色,虽然对象之间有引用,但是没有GC Roots集合中的对象关联,所以在判定时会认为橙色为可回收对象。

二、GC Root对象

可作为GC Roots的对象包括以下几种:
1、在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
2、在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
3、在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
4、所有被同步锁(synchronized关键字)持有的对象。
5、在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
6、Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
7、反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
在代码层面,我们主要关心1、2、3、4点,下面我们来看看具体场景。

三、使用场景

1、虚拟机栈中局部变量表引用的对象

方法的执行实际上是一个栈帧在虚拟机栈中一次进栈和出栈的过程,栈帧的其中一个结构就是局部变量表。
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译成Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的最大局部变量表的容量。局部变量表的容量以变量槽(Slot)为最小单位,32位虚拟机中一个Slot可以存放一个32位以内的数据类型(boolean、byte、char、short、int、float、reference和returnAddress八种)。注意:局部变量表并不是根据方法中定义的变量的数量来决定其最大容量的,它允许变量槽重用,例如以下代码局部变量表的最大容量是1,而不是2.

public static void test() {
        try {
            int i = 0;
        } catch (Exception e) {
            e.printStackTrace();
        }
        int j = 1;
    }

OK,在大致了解了局部变量表的作用后,我们来看一段具体代码,首先贴出在运行程序时,需要设置的虚拟机参数:

-Xmx20m -Xms20m -XX:+PrintGCDetails

该参数的含义为设置堆的最大、最小容量,同时输出GC的详细信息。
场景一:

public class Test02 {
    public static void main(String[] args) {
        byte[] b1 = new byte[1024*1024*5];
        //提醒虚拟机需要进行一次垃圾回收,注意,只是提醒,并不是立刻强制执行
        System.gc();
    }
}

得到的垃圾回收日志如下:

在这里插入图片描述
本次创建的对象大小为5M,按照分配策略,大对象是分配到老年代的。此时从图中可以看出老年代中还有约5M大小的内存未被释放。我们结合字节码来讲述这一现象,字节码如图所示:
在这里插入图片描述
newarray指令是在队中创建一个大小为5242880大小的数组。然后astore_1是将该数组的引用保存在main方法栈帧的局部变量表1号变量槽的位置。所以呀,此时局部变量表中包含了该数组对象的引用,以至于在进行垃圾回收时,会判定该对象时不可被回收对象。

场景二,代码如下:

public class Test02 {
    public static void main(String[] args) {
        try{
            byte[] b1 = new byte[1024*1024*5];
        }catch (Exception e){

        }
        int i = 0;
        System.gc();
    }
}

得到的GC日志如下:
在这里插入图片描述
从图中可以发现,在老年代中已经将数组所占用空间回收了,这是因为b1的作用域仅仅只是在try语句块中,上述我们说过,局部变量表槽会重用,所以呀,b1的变量槽,在执行到int i = 0这一行代码时,已经被i变量用了,局部变量表中自然也不再包含该对象的引用,自然该数组对象就被回收了呀。如图所示:
在这里插入图片描述
针对场景二,再来看看场景三。
场景三,代码如下:

public class Test02 {
    public static void main(String[] args) {
        try{
            byte[] b1 = new byte[1024*1024*5];
        }catch (Exception e){

        }
        System.gc();
    }
}

得到的GC日志如图所示:
在这里插入图片描述
从图中可以看到,该数组并没有被回收,这也印证了咱们对场景二的分析。因为在定义该变量之后,就没有再对局部变量表进行存取了,该数组的引用也一直保存在局部变量表中。以至于GC时不能将该数组所占孔吉安回收掉。所以呀,我们在程序开发过程中,一定要注意,不要定义无用的变量,这样会白白耗费内存资源。怎么样,有趣吧。

2、方法区中类静态属性引用的对象

public class Test02 {
    public static void main(String[] args) {
        try{
            byte[] b1 = new byte[1024*1024*5];
            Person.bytes = b1;
        }catch (Exception e){

        }
        int i = 0;
        System.gc();
    }
}
class Person{
    public static byte[] bytes;
}

GC日志如图所示:
在这里插入图片描述

按照场景二的描述,该数组所占空间应该被回收,但是我们将其赋值给Person类的静态属性,以至于在GC时,它不会被回收。
写到此处,笔记本没电了,换了一台电脑继续写,下文使用eclipse。

3、在方法区中常量引用的对象

代码如下:

public class Test03 {
    private static final byte[] b1 = new byte[5*1024*1024];
    public static void main(String[] args) {
        Test03 test03 = new Test03();
        test03 = null;
        System.gc();
    }
}

得到的GC日志如图所示:
在这里插入图片描述
此处,博主也有一个一个,我一直尝试去证实这一点:“譬如字符串常量池(String Table)里的引用”,但是没法复现,如果哪位小伙伴写出了代码,可评论区留言哦。

4、所有被同步锁(synchronized关键字)持有的对象

代码如下:

public class Test03 {
    public static void main(String[] args) {
        try {
            byte[] bytes = new byte[5 * 1024 * 1024];
            Target target = new Target(bytes);
            new Thread(target).start();
        } catch (Exception e) {
            
        }
        int i = 0;
        //此处是为了让主线程等待Target开始执行,等待将b1置为null操作。
        try {
            Thread.sleep(500);
        } catch (Exception e) {
            
        }
        System.gc();
    }
}

class Target implements Runnable {
    private byte[] b1;

    public Target(byte[] b) {
        this.b1 = b;
    }

    @Override
    public void run() {
        synchronized (b1) {
            b1 = null;
            try {
            //此处休眠是为了让Target任务停留在synchronized代码块中
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

得到的GC日志如下:
在这里插入图片描述
我们可以看到,在同步代码块内,我们将b1置为null,但是该数组所占空间并没有被回收掉,这是因为该对象被synchronized持有。为了印证该结论,我们将代码简单变化,如下:

public class Test03 {
    public static void main(String[] args) {
        try {
            byte[] bytes = new byte[5 * 1024 * 1024];
            Target target = new Target(bytes);
            new Thread(target).start();
        } catch (Exception e) {

        }
        int i = 0;
        try {
            Thread.sleep(500);
        } catch (Exception e) {

        }
        System.gc();
    }
}

class Target implements Runnable {
    private byte[] b1;

    public Target(byte[] b) {
        this.b1 = b;
    }

    @Override
    public void run() {
        b1 = null;
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

得到的GC日志如下:
在这里插入图片描述
可以看到,该数组对象所占空间已经被回收了。
从代码层面,大致的场景就是这些,在实际开发中定位到内存问题,大家可以尝试从这些方面去寻找原因。好啦,本文就讲到这儿,如果这篇文章帮助到您,请点赞支持。谢谢大家。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值