Java虚拟机--如何确定需要回收的垃圾对象

1. 何为对象的引用?

Java中的垃圾回收一般是在Java堆中进行,因为堆中几乎存放了Java中所有的对象实例。在java中,对引用的概念简述如下(引用强度依次减弱) :

  • 强引用: 这类引用是Java程序中最普遍的,只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
  • 软引用: 用来描述一些非必须的对象,在系统内存不够使用时,这类对象会被垃圾收集器回收,JDK提供了SoftReference类来实现软引用。
  • 弱引用: 用来描述一些非必须的对象,只要发生GC,无论但是内存是否够用,这类对象就会被垃圾收集器回收,JDK提供了WeakReference类来实现弱引用。
  • 虚引用: 与其他几种引用不同,它不影响对象的生命周期,如果这个对象是虚运用,则就跟没有引用一样,在任何时刻都可能会回收,JDK提供了PhantomReference类来实现虚引用。

示例代码:

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;

/**
 * @author xxx
 * @date 2020-07-03 10:11
 **/
public class ReferenceDemo {

    public static void main(String[] args) {

        // 强引用
        Object object = new Object();
        Object[] objects = new Object[10];

        // 软引用
        SoftReference<String> stringSoftReference = new SoftReference<>("sss");
        System.out.println(stringSoftReference.get());
        System.gc();
        // 手动gc,这是内存充足,对象没有被回收
        System.out.println(stringSoftReference.get());

        // 弱引用
        WeakReference<String> stringWeakReference = new WeakReference<>("www");
        System.out.println(stringWeakReference.get());
        System.gc();
        // 手动gc,返回null,对象已经被回收
        System.out.println(stringWeakReference.get());

        // 虚引用
        // 虚引用主要用来跟踪对象被垃圾回收器回收的活动。
        // 虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。
        // 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
        ReferenceQueue<String> stringReferenceQueue = new ReferenceQueue<>();
        PhantomReference<String> stringPhantomReference = new PhantomReference<>("ppp", stringReferenceQueue);
        System.out.println(stringPhantomReference.get());

    }
}

2. 如何确定需要回收的垃圾对象?

2.1. 引用计数器

每个对象都有一个引用计数器 , 新增一个引用的时候就+1,引用释放的时候就-1,当计数器为0的时候,就表示可以回收。引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个
不错的选择,不过Java语言并没有选择这种算法来进行垃圾回收,主要原因是它很难解决对象之间的相互循环引用问题。

public class LoopReferenceDemo {
    
    public static void main(String[] args) {
        TestA a = new TestA(); //1
        TestB b = new TestB(); //2
        a.b = b; //3
        b.a = a; //4
        a = null; //5
        b = null; //6
    }
}

class TestA {
    public TestB b;
}

class TestB {
    public TestA a;
}

第一行 : TestA的引用计数器加1,TestA的引用数量为1
第二行 : TestB的引用计数器加1,TestB的引用数量为1
第三行 : TestB的引用计数器加1,TestB的引用数量为2
第四行 : TestA的引用计数器加1,TestA的引用数量为2

内存分布如下图:

第五行 : 将a变量设置为null,不再指向堆中的引用,所以TestA的引用计数器减1,TestA的引用数量为1
第六行 : 将b变量设置为null,不再指向堆中的引用,所以TestB的引用计数器减1,TestB的引用数量为1

内存分布如下图:

 结论 : 虽然上面程序将局部变量a和b设置为null了,但是在堆中,TestA和TestB还是互相持有对方的引用,引用计数器依然不等于0,这个就称为循环引用,所以说"引用计数器"
会存在这个问题,导致这类对象无法被清理掉。

2.2. 可达性分析

目前主流的虚拟机都采用"可达性分析(GC Roots Tracing)"算法来标记哪些对象是可以被回收的。
该算法是从 GC Roots 开始向下搜索,搜索走过的路径称之为引用链。当一个对象到 GC Roots 没有任何引用链相连时,就代表这个对象是不可用的,称为"不可达对象"。

GC Roots包括:

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量(表达式内的变量是临时变量, `for(int  i=0){}` i 就是临时变量.)等。
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  • 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  • 所有被同步锁(synchronized关键字)持有的对象。
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

实际上,要真正宣告一个对象死亡,至少要经历两次标记过程 :
如果对象在进行根搜索后发现没有与GC Roots相连接的引用链,那它会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆
盖finalize()方法,或finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。如果该对象被判定为有必要执行finalize()方法,那么这个对象将会被放置
在一个名为F-Queue队列中,并在稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行finalize()方法。finalize()方法是对象逃脱死亡命运的最后一次机会(因为
一个对象的finalize()方法最多只会被系统自动调用一次),稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果要在finalize()方法中成功拯救自己,只要在finalize()方
法中让该对象重引用链上的任何一个对象建立关联即可。而如果对象这时还没有关联到任何链上的引用,那它就会被回收掉。

 

https://my.oschina.net/wangkang80/blog/1559071

posted @ 2020-07-06 16:08  景岳  阅读(419)  评论(0编辑  收藏  举报