垃圾回收:第一篇

2020-10-30   35 次阅读


垃圾回收

我们知道,开发Java程序的时候,Java程序员一般是不需要关注对象的回收的,而是由Java的垃圾回收机制帮助我们自动回收掉没用的对象,JVM提供了多种垃圾回收算法,以及多种垃圾回收策略,不同的回收算法以及策略有不同的适用场景,如果在项目中使用了不合适的垃圾回收算法或者策略,那么系统的性能将很难达到最优,在某些业务场景下,不合适的垃圾回收算法或者策略,甚至可能会导致性能的大幅下降,所以说,垃圾回收的重要性是毋庸置疑的;

什么场景下该使用什么垃圾回收策略?

  • 在对内存要求苛刻的场景:想办法提供对象的回收效率,多回收掉一些对象,腾出更多内存
  • 在CPU使用率高的情况下:降低高并发时垃圾回收的频率,让CPU更多地去执行你的业务而不是垃圾回收

垃圾回收发生在哪些区域?


虚拟机栈、本地方法栈以及程序计数器都是线程独享的,这三个区域随着线程的创建而创建,随着线程的销毁而销毁,而栈里面的栈帧又会随着方法的进入和退出分别进行入栈和出栈的操作,所以呢,这三块区域是不需要考虑垃圾回收的;
堆和方法区是线程共享的,这两块区域才需要关注垃圾回收,而堆是垃圾回收的主要区域,主要是回收我们创建的对象,而方法区则可以回收废弃的常量以及不需要使用的类

对象在什么时候能够被回收(如何判断对象是否可以回收)?

第一种:引用计数法

  • 通过对象的引用计数器来判断该对象是否被引用
    比方说,对于某一个对象A,只要有一个对象引用了A,那么A的引用计数器就会+1,当引用失效的时候,A上面的引用计数器又会-1,如果A的引用计数器值变成0的话,就说明这个对象已经没有引用了,可以回收,例如下图:
    image.png
    A引用B,在B上可以找到A的引用,B的引用计数是1,B引用C,在C上可以找到B的引用,C的引用计数也是1,要想使用引用计数法还是比较简单的,使用引用计数法去判断对象是否可以回收,效率也是比较高的;如果对象之间存在循环引用的话,使用引用计数法就无能为力了,例如下图:
    image.png
    A引用B,B引用C,C引用D,D又引用B,在这个情况下B上可以找到A的引用和D的引用,B的引用计数是2,某一天A不引用B了,理论上BCD都要被回收,但是我们发现B上可以找到D的引用,C可以找到B的引用,D可以找到C的引用,BCD的引用计数都是1,那么这种情况下使用引用计数法的话就会导致BCD都无法回收,由于引用计数法处理循环引用存在问题,所以就目前来说,Java并没有使用引用计数法;
    Java采用的是第二种算法,叫做可达性分析

第二种:可达性分析

  • 思路:以跟对象(GC Roots)作为起点向下搜索,走过的路径被称为引用链(Reference Chain),如果某个对象到根对象没有引用链相连时,就认为这个对象是不可达的,可以回收,用图表示大概如下:
    image.png
    对象1234都是可达的,不可以被回收,而对象567没有到跟对象的引用链存在,所以都可以回收
GC Roots包括哪些对象?(那些对象可以作为跟对象?)

根对象主要包括以下几类

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象(一般是局部变量)
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即Native方法)引用的对象
引用有哪些?
  • 强引用(Strong Reference)
    • 形如Object obj = new Object()的引用
    • 只要强引用在,垃圾回收器永远不会回收被引用的对象(内存溢出也不例外)
  • 软引用(Soft Reference)
    • 形如SoftReference sr = new SoftReference<>("hello")
    • 是用来描述一些有用但非必需的对象
    • 软引用关联的对象,只有在内存不足的时候JVM才会回收(根据这个特效,软引用就比较适合用来实现缓存,比方说网页缓存,图片缓存等等)
  • 弱引用(Weak Reference)
    • 形如WeakReference sr = new WeakReference<>("hello")
    • 弱引用也是用来描述非必须对象的
    • 当JVM进行垃圾回收的时候,无论内存是否充足,都会回收被弱引用关联的对象
  • 虚引用(Phantom Reference)
    • 形如ReferenceQueue queue = new ReferenceQueue<>();
      PhantomReference pr = new PhantomReference<>("hello", queue);
    • 不影响对象的生命周期,如果一个对象只有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收;虚引用主要用来跟踪对象被垃圾回收器回收的活动,必须和引用队列(ReferenceQueue)配合使用;当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中;程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收;如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动
可达性算法注意点
  • 一个对象即使不可达,也不一定会被回收
    对象不可达,只是给这个对象判了死缓,要想猝死这个对象,完整的流程大概如下:
    image.png
    在做垃圾回收的时候,首先会判断对象有没有和跟对象的引用链,如果存在引用链就不回收,如果对象没有引用链和GCRoots连接,就会给这个对象判死缓;
    那么怎么给这个对象判死刑呢?JVM首先会去判断有没有必要去执行这个对象的finalize()方法;
    如果没有必要执行对象的finalize()方法,那么就直接回收,如果你的对象没有重写过finalize()方法或者是虚拟机已经调用过finalize()方法,都会认为没有必要再去执行finalize()方法;
    如果判断出来有必要执行finalize()方法,那么就会把这个对象放入一个名为F-Queue的队列里面去,然后会由虚拟机自动建立一个低优先级的finalizer线程,去执行这个对象的finalize()方法,如果你在finalize方法里面重新和引用链上的任意一个对象建立连接,那么这个对象就会从F-Queue里面移除不回收,因为你已经重新建立连接了,这个对象又变成可达的了,如果调用过finalize方法后,这个对象还是没有到达根对象的引用链,那么这个对象也会被回收;

  • 代码演示

# 演示代码
public class GC01Test {
    private static GC01Test obj;

    // 重写finalize方法
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize被调用了");
        obj = this;
    }

    public static void main(String[] args) 
        throws InterruptedException {
        // 对象赋值
        obj = new GC01Test();
        obj = null;
        // 第一次 手动执行gc
        System.gc();

        // 第一次 休眠后判断对象是否可用
        Thread.sleep(1000);
        if(obj == null){
            System.out.println("第一次 obj == null");
        } else {
            System.out.println("第一次 obj可用");
        }

        // 第二次 休眠后判断对象是否可用
        Thread.sleep(1000);
        obj = null;
        // 第二次 手动执行gc
        System.gc();
        if(obj == null){
            System.out.println("第二次 obj == null");
        } else {
            System.out.println("第二次 obj可用");
        }
    }
}

# 执行结果
finalize被调用了
第一次 obj可用
第二次 obj == null

类重写了finalize方法,并且在这个方法里面把this赋给了obj对象;
然后在main方法里面,先new了这个obj,在把obj设成null去掉了对象的引用,然后使用 System.gc()强制回收这个对象;
代码执行到这里,按照上面的讲解,首先会调用finalize方法,把对象扔到F-Queue队列里面去,因为这里我们重写了finalize方法,所以不会走无必要这个分支;
但是在finalize方法里面我们把this赋给了obj,也就是说finalize方法里面,我们又重新创建了对象的引用,所以会导致这个对象会从F-Queue里面移除没有被回收,因此在这里会打印 第一次 obj可用
在执行第二次休眠后 再次把obj对象设置为null去除对象的引用,并且再次触发System.GC(),因为之前已经执行过finalize方法,所以这次会走无必要分支,因此这一次对象被回收了;

经过分析,如果第二次我们没有把obj设置成null(obj == null)的话,就会导致obj对象永远无法回收,所以实际项目里面一般建议

  • 避免使用finalize()方法,操作不当可能会导致问题
  • finalize()优先级低,何时会被调用无法确定,因为什么时间发生GC不确定(上述演示代码是通过System.gc()强制触发垃圾回收的)
  • 实际项目里面,建议使用try...catch...finally来代替finalize()

Q.E.D.

知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议

那一年,我也变成了光!!