【深入理解Java虚拟机】二、垃圾回收器 如何判断对象已"死''-引用计数法、可达性分析法详解

文章目录

  • 1.垃圾回收器
    • 1.1 如何判断对象已“死”
      • 1.1.1 引用计数法
      • 1.1.2 可达性分析算法
    • 1.2 回收方法区

1.垃圾回收器

对于程序计数器、虚拟机栈、本地方法栈这三部分区域而言,其生命周期与相关线程有关,随线程而生,随线程而灭,这三个区域的内存分配与回收具有确定性,当方法结束或者线程结束时,内存就自然而然跟着回收了。
所以我们谈的垃圾回收关注的是Java堆和方法区这两个区域

1.1 如何判断对象已“死”

Java堆中存放着几乎所有的对象实例【如果虚拟机通过分析得出对象不会存在方法逃逸或者线程逃逸、考虑直接在栈帧中为该对象分配空间】,垃圾回收器在对堆进行垃圾回收前,首先判断这些对象哪些还活着,哪些已经死去。

1.1.1 引用计数法

  给对象增加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已“死”。
  引用计数器实现方法简单,效率也比较高,但是主流的JVM并没有选用引用计数法来管理内存,最主要的原因是引用计数器无法解决对象的循环引用问题。
在这里插入图片描述
即像A调用B,B又调用A这种情况没有办法解决。

1.1.2 可达性分析算法

Java中采用可达性分析算法来判断对象是否存活。
此方法的核心思想是:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达),证明此对象是不可用的。
【深入理解Java虚拟机】二、垃圾回收器 如何判断对象已对象Object5-Object7之间虽然彼此还有关联,但是它们到GC Roots是不可达的,因此他们会被判定为可回收对象。

在Java语言中,可作为GC Roots的对象包含下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量(final static)引用的对象
  • 本地方法栈中引用的对象

  在JDK1.2之前,Java中引用的定义很传统:如果引用数据类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。但是在这种定义下只有被引用或者没有被引用两种状态

  我们希望描述对象时:当内存空间还足够时,能保存在内存中;如果内存空间在进行垃圾回收后还是非常紧张,则可以抛弃这些对象。

  在JDK1.2之后,Java对引用的概念做了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种,这四种引用的强度依次递减

  • 强引用:指的是在程序代码之中普遍存在的,类似于"Object obj = new Object()"这类的引用,只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象实例。
  • 软引用:软引用是用来描述一些还有用但是不是必须的对象,对于软引用关联着的对象,在系统将要发生内存溢出之前,会把这些对象列入回收范围之中进行第二次回收(先标记后回收–二次回收),如果这次回收还是没有足够的内存,才会抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类来实现弱引用
  • 弱引用:弱引用也是用来描述非必须要对象的。但是它的强度弱于软引用,被弱引用关联的对象只能生存到下一次垃圾回收发生之前(一旦GC就回收–一次回收),当垃圾回收器开始进行工作时,无论当前内容是否够用,都会回收掉被弱引用关联的对象
  • 虚引用:也被称为幽灵引用或者幻影引用,它是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例,为一个对象设置虚引用的唯一目的就是能在这个对象被垃圾回收时收到一个系统通知。(监听到垃圾回收机制)

生存还是死亡?
  可达性分析算法中的不可达对象,也并不是"非死不可"的,这时候他们暂时处在"缓刑阶段"。要宣告一个对象的真正死亡,至少要经历两次标记过程:如果对象在进行可达性分析之后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行**finalize()**方法,当对象没有覆盖finalize()方法或者finalize()方法已经被JVM调用过了,虚拟机将这两种情况都视为"没有必要执行",此时的对象才是真正’死’的对象。
  如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的队列之中,并由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它(即就是虚拟机触发finalize()方法),finalize()方法是对象逃脱死亡的最后一次机会,如果对象在finalize()中成功拯救自己 (只需要与引用链上的任何一个对象建立起关联关系即可),那在第二次标记时它将会被收回(finalize()方法只能自救一次哦);如果对象在finalize()中没有成功拯救自己,就会被立刻被收回。
范例:对象的自我拯救过程

public class Test {
    public static  Test test;
    public void isAlive(){
        System.out.println("线程活着");
    }
    @Override
    public void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize方法执行");
        //与引用链上的任何一个对象建立起关联关系
        test = this;
    }

    public static void main(String[] args) throws InterruptedException {
        //1.在堆上创建对象
        test = new Test();
        //2.test置空 堆上的对象没有任何栈内存指向
        test = null;
        //3.调用垃圾回收机制 但是由于此对象覆写了finalize方法 可以缓刑
        System.gc();
        //4.垃圾回收需要时间
        Thread.sleep(500);
        if(test != null){
            test.isAlive();
        }else{
            System.out.println("线程死亡");
        }
        //------------------------------------------
        //下面的代码与上面完全一致,但是此时自救失败
        test = null;
        System.gc();
        Thread.sleep(500);
        if(test != null){
            test.isAlive();
        }else{
            System.out.println("线程死亡");
        }
    }
}

【深入理解Java虚拟机】二、垃圾回收器 如何判断对象已
若上述代码中没有finalize()方法或者finalize()方法中的test没有与引用链中的任何一个对象建立联系,对象就会在第一个次gc的时候被回收。
【深入理解Java虚拟机】二、垃圾回收器 如何判断对象已
任何一个对象的finalize()方法都只会被系统调用一次,如果相同的对象在逃脱一次后又面临一次回收,它的fianlize()方法不会被再次执行。

1.2 回收方法区

方法区的垃圾回收主要收集两部分内容:废弃常量和无用的类
回收废弃常量和回收Java堆中的对象十分类似,以常量池的回收为例,假如一个字符串"abc"已经进入了常量池,但是当前系统中没有任何一个String对象引用常量池中的abc常量,也没有在其他地方引用这个字面量,如果此时发生GC并且有必要的话,这个“abc”常量会被系统清理出常量池,常量池中的其他类、接口、方法、字段的符号引用也与此类似。
判定一个类是否是"无用类"则相对复杂的多,需要同时满足以下三个条件:

  • 该类的所有实例都已经被回收了(java堆中不存在该类的实例)
  • 加载该类的ClassLoader已经被回收
  • 该类对应的Class对象没有在任何其他地方被引用,无法在任何地方通过反射访问该类的方法
    JVM可以对满足上述3个条件的无用类进行回收,也仅仅是"可以"而不是必然。在大量使用反射、动态代理等场景都需要JVM具备类卸载的功能来放置永久代的溢出。

你可能感兴趣的:(Java,Java学习)