JVM垃圾回收系列之对象垃圾判别

随笔

最近突然有点慢慢明白以前听某位大佬说过的一句话“大部分程序员都不具备解决困难问题的能力”,忘记是从哪听来的了。至于最近为什么有这种感悟,是当自己通过书籍、博客学习新知识内容的时候,总感觉自己跟大佬的差距像是有一道鸿沟,将我和他们隔离开来,我其实是非常羡慕并想努力向大佬们的靠的更近些,但是随着自己不断填充知识,弥补技术栈的缺失的同时却越发觉得自己面对真正困难问题时的不堪一击。
在这里插入图片描述

引言

这篇博文将介绍HotSpot虚拟机中判别垃圾的两种算法“引用计数法、根可达算法”。

参考书籍:“深入理解Java虚拟机”

个人java知识分享项目——gitee地址

个人java知识分享项目——github地址

引用计数法

在每个java对象中都有引用计数器,这个计数器的作用是每当有一个引用指向这个对象,那么这个计数器就会加一,反之,每减少一个引用指向这个对象,这个计数器就会减一。所以在任何一个时刻计数器为0的对象就是不可能被使用的对象,因为没有任何地方持有这个引用,这时这个对象就被视为内存垃圾,等待被虚拟机回收。

优点:

从客观的角度来说,这种方法很有效地去判别了对象是否是垃圾对象,在大部分的情况下都是适用的。实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。

缺点:

它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
在某些特别的场景下无法适用,如:在循环引用的情况下。如下图中,对象一中有属性指向对象二,对象二中有属性指向一,如此反复,那么每个对象中的引用计数器都不可能是0,但是这些对象在系统中是无法发挥作用的,那么就是垃圾应该进行清理,但是如果只是使用引用计数器算法是无法解决这个问题的!

在这里插入图片描述

也正是由于这种算法的无法解决循环引用的问题,博主所知道的虚拟机中是没有采用这种垃圾识别算法的。

案例:

/*
引用计数算法
下方的例子就是为了证明hotspot不是使用的引用计数算法来进行垃圾判别
 */
public class GCAlgorithmDemo1 {
    
    
    public static void main(String[] args) {
    
    
        test test1 = new test();
        test test2 = new test();
        test1.t = test2;
        test2.t = test1;
        test1=null;
        test2= null;
        System.gc();
    }
}
class test{
    
    
    public byte[] bytes = new byte[1024*1024*10];
    public  test t;
}

可达性分析算法

这种算法通过一系列成为"GCRoots"的对象作为起始点,从这些节点开始向下搜索所有走过的路径成为引用链(ReferenceChain),当一个对象GCRoots没有任何引用链相连(用图论的话来说就是从GCRoots到这个对象不可达),则证明此对象是不可用的

如下图所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达 的,所以它们将会被判定为是可回收的对象。
在这里插入图片描述
在Java语言中,可作为GC Roots的对象包括下面几种:

  1. 在虚拟机栈(其实是栈帧中的本地变量表)中引用的对象
  2. 在方法区中的类静态属性引用对象
  3. 在方法区中的常量池中的常量引用的对象
  4. 在本地方法栈中JNI(即一般说的Native方法)的引用对象
  5. 同步锁synchronized锁的对象

java中的四种引用类型

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。在JDK 1.2以前,Java中的引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。

在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱:

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

对象的生存还是死亡

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处 于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选, 筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。

如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做 F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统 崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。

案例:

public class GCAlgorithmDemo2 {
    
    
    static GCAlgorithmDemo2 test1 = new GCAlgorithmDemo2();
    static GCAlgorithmDemo2 test2 = new GCAlgorithmDemo2();

    public static void main(String[] args) {
    
    
        System.out.println("是否清除对象?");
        new Scanner(System.in).next();
        test1 = null;
        test2 = null;
        System.gc();
        try {
    
    
            Thread.sleep(2000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        if (test1 == null && test2 == null) {
    
    
            System.out.println("内存清除完毕,类并没有重写finalize方法,直接进入不可触及状态");
        } else {
    
    
            System.out.println("重写了finalize方法,进入可重生状态");
        }
        System.out.println("是否再次清除对象?");
        new Scanner(System.in).next();
        test1 = null;
        test2 = null;
        System.gc();
        try {
    
    
            Thread.sleep(2000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        if (test1 == null && test2 == null) {
    
    
            System.out.println("内存清除完毕");
        }
    }

    @Override
    protected void finalize() throws Throwable {
    
    
        super.finalize();
        System.out.println("调用finalize方法");
        test1 = this;
        test2 = this;
    }
}

对于这个案例就不过多讲解了。

方法区中的垃圾判别

很多人认为方法区(或者HotSpot虚拟机中的永久代)是没有垃圾收集的,Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区中进行垃圾收集 的“性价比”一般比较低;在堆中,尤其是在新生代中,常规应用进行一次垃圾收集一般可以回收70%~95%的空间,而永久代的垃圾收集效率远低于此。

永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。回收废弃常量与回收 Java堆中的对象非常类似。以常量池中字面量的回收为例,假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象是叫做“abc”的,换句话说,就是没有任何 String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,如果这时发生内存回收,而且必要的话,这个“abc”常量就会被系统清理出常量池。常量池中的其他类(接 口)、方法、字段的符号引用也与此类似。

判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是“无用的类”:

  1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
  2. 加载该类的ClassLoader已经被回收。
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该 类的方法。

虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是 和对象一样,不使用了就必然会回收。是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc 参数进行控制,还可以使用-verbose:class以及-XX:+TraceClassLoading、-XX: +TraceClassUnLoading查看类加载和卸载信息,其中-verbose:class和-XX: +TraceClassLoading可以在Product版的虚拟机中使用,-XX:+TraceClassUnLoading参数需要 FastDebug版的虚拟机支持。

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

猜你喜欢

转载自blog.csdn.net/a_ittle_pan/article/details/126814872