Skip to content

堆的垃圾回收

1. 垃圾回收条件

Java中的对象是否能被回收,是根据对象是否被引用来决定的。如果对象被引用了,说明该对象还在使用,不允许被回收。 Alt text 图中A的实例对象要回收,有两个引用要去除:
1.栈中a1变量到对象的引用 2.B对象到A对象的引用

java
a1=null;
b1.a=null;

如果在main方法中最后执行a1 = null ,b1 = null,是否能回收A和B对象呢?
答案是可以回收,方法中已经没有办法使用引用去访问A和B对象了(也就是对象回收主要看虚拟机栈帧中变量表里面的变量到堆的引用关系还在不在)。

2. 判断堆上对象被引用方法

常见的有两种判断方法:引用计数法和可达性分析法。

2.1 引用计数法

引用计数法会为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1。 Alt text

2.2 引用计数法的缺点

引用计数法的优点是实现简单,C++中的智能指针就采用了引用计数法,但是它也存在缺点,主要有两点:

  1. 每次引用和取消引用都需要维护计数器,对系统性能会有一定的影响
  2. 不能解决循环引用场景,所谓循环引用就是当A引用B,B同时引用A时会出现对象无法回收的问题。
    Alt text
java
public class Jvm10 {

    public static void main(String[] args) throws Exception {
        while (true){
            A a1 = new A();
            B b1 = new B();
            a1.b = b1;
            b1.a = a1;
            a1 = null;
            b1 = null;
            System.gc();
        }
    }

}

class A {
  B b;
}

class B {
    A a;
}

我们执行发现程序的内存并没有在增加 Alt text 如果加上-verbose:gc参数查看gc信息,可以看到占用一直是239K:
Alt text 说明此时JVM垃圾回收能够处理互有依赖关系的对象,因为JVM底层并没有使用引用计数法进行内存回收,而是使用可达性分析算法实现。

2.3 可达性分析算法

Java使用的是可达性分析算法来判断对象是否可以被回收。可达性分析将对象分为两类:垃圾回收的根对象(GC Root)和普通对象。GC Root对象一般是不可以被回收的,JVM会持有一个GC Root对象列表,可达性分析算法就是从GC Root对象出发,通过它的引用链找到的对象也是不可回收的。
下图中A到B再到C和D,形成了一个引用链,可达性分析算法指的是如果从某个到GC Root对象是可达的,对象就 不可被回收。
Alt text

2.4 GC Root对象分类

如何区分GC Root对象和普通对象呢? GC Root对象分类主要为一下这些:

  • 线程Thread对象。
  • 系统类加载器加载的java.lang.Class对象。
  • 监视器对象,用来保存同步锁synchronized关键字持有的对象。
  • 本地方法调用时使用的全局对象(JVM内部的,暂不考虑)。

比如之前的代码程序例子,a1,b1都是在main的栈帧里面的,而main栈帧和主线程关联的,所以a1,b1能够找到堆里面的主线程对象并建立关联关系。当a1和b1都置为null时,堆里面的关联a1,b1的对象对于线程对象(Gc Root)时不可达的,就会被回收。 Alt text sun.misc.Launcher类Class对象就是GC Root对象 Alt text 当使用synchronized关键字的时候,监视器对象就和Class类关联上了,从而Class类不会被卸载回收。 Alt text

2.5 查看GC Root

通过arthas和eclipse Memory Analyzer (MAT) 工具可以查看GC Root,MAT工具是eclipse推出的Java堆内存检测工具。具体操作步骤如下:
1、使用arthas的heapdump命令将堆内存快照保存到本地磁盘中。
2、使用MAT工具打开堆内存快照文件。
3、选择GC Roots功能查看所有的GC Root。

使用heapdump命令导出当前堆内存快照 Alt text 启动MAT工具,点击File->Open Head Dump->选择jvm01.hprof文件进行导入:
Alt text 导入后,点击查看GC Roots:
Alt text 可以看到GC Roots就是分为4大类,查看GC Roots中的Thread类,选着main线程,点击进去可以看到很多关联关系的对象。也就是这些不可被回收的对象有这些。 Alt text 如果要查看系统类加载器加载的java.lang.Class对象作为的GC Roots有哪些,看起来有很多2千多个:
Alt text 这时如果想看指定的对象所有有引用关系的GC Roots就不太方便,可以这么操作:
Alt text 然后点击Path to GC Roots->with all reference:
Alt text 粘贴刚才拷贝的地址,点击Flinish Alt text 可以看到当前对象UserInfo有1个GC Roots:
Alt text

3. 对象引用

是不是只有这种情况对象才能被回收呢?
也不是的!
可达性算法中描述的对象引用,一般指的是强引用,即是GCRoot对象对普通对象有引用关系,只要这层关系存在, 普通对象就不会被回收。除了强引用之外,Java中还设计了几种其他引用方式:

  • 软引用
  • 弱引用
  • 虚引用
  • 终结器引用

4. 软引用

软引用相对于强引用是一种比较弱的引用关系,如果一个对象只有软引用关联到它,当程序内存不足时,就会将软 引用中的数据进行回收。在JDK1.2版之后提供了SoftReference类来实现软引用,软引用常用于缓存中。
Alt text

4.1 软引用执行过程

软引用的执行过程如下:
1.将对象使用软引用包装起来,new SoftReference<对象类型>(对象)。
2.内存不足时,虚拟机尝试进行垃圾回收。
3.如果垃圾回收仍不能解决内存不足的问题,回收软引用中的对象。
4.如果依然内存不足,抛出OutOfMemory异常。

java
// Caffeine是基于Java1.8的高性能本地缓存库,由Guava改进而来,
// 而且在Spring5开始的默认缓存实现使用了Caffeine
Cache<Object, Object> cache = Caffeine.newBuilder().softValues().build();

其中softValues()方法就是使用了软引用的方式构建缓存。

java
public class SoftReferenceDemo1 {
    public static void main(String[] args) {
        byte[] bytes1 = new byte[1024*1024*100];
        SoftReference<byte[]> softReference = new SoftReference<>(bytes1);
        bytes1 = null;
        System.out.println(softReference.get());

        byte[] bytes2 = new byte[1024*1024*100];
        System.out.println(softReference.get());
    }
}

运行加上-Xmx200m参数,可以发现第二次打印软引用对象发生变化:
Alt text 此时SoftReference里面的byte数组为空,那么SoftReference对象本身也是可以回收的,但我们直接在代码里面优化:

java
if (softReference.get() == null) {
    softReference = null;
}

但是呢这样不太灵活,SoftReference对象需要我们自己去管理了,但是不好管理,真实生产环境里面我们是不知道那个时候软引用中的对象被回收了,不可能像这里的代码刚刚好就被回收。

4.2 SoftReference队列机制

软引用中的对象如果在内存不足时回收,SoftReference对象本身也需要被回收。如何知道哪些SoftReference对 象需要回收呢?
SoftReference提供了一套队列机制:
1、软引用创建时,通过构造器传入引用队列
2、在软引用中包含的对象被回收时,该软引用对象会被放入引用队列
3、通过代码遍历引用队列,将SoftReference的强引用删除
Alt text

java
public class SoftReferenceDemo2 {
    public static void main(String[] args) {

        ReferenceQueue<byte[]> referenceQueue = new ReferenceQueue<>();
        byte[] bytes1 = new byte[1024 * 1024 * 100];
        SoftReference<byte[]> softReference1 = new SoftReference<>(bytes1, referenceQueue);

        bytes1 = null;
        byte[] bytes2 = new byte[1024 * 1024 * 100];
        SoftReference<byte[]> softReference2 = new SoftReference<>(bytes2, referenceQueue);

        bytes2 = null;
        byte[] bytes3 = new byte[1024 * 1024 * 100];
        SoftReference<byte[]> softReference3 = new SoftReference<>(bytes3, referenceQueue);

        int count = 0;
        Reference<? extends byte[]> ref;
        while ((ref=referenceQueue.poll())!=null){
            ++count;
            System.out.println(ref);
        }
        System.out.println(count);
    }
}

运行结果:
Alt text referenceQueue里面就是负责保存已经回收bytes的SoftReference对象,借助引用队列就可以回收SoftReference对象了。

5. 弱引用

弱引用的整体机制和软引用基本一致,区别在于弱引用包含的对象在垃圾回收时,不管内存够不够都会直接被回收。 在JDK1.2版之后提供了WeakReference类来实现弱引用,弱引用主要在ThreadLocal中使用。弱引用对象本身也可以使用引用队列进行回收。
Alt text

java
public class WeakReferenceDemo1 {
    public static void main(String[] args) {
        byte[] bytes1 = new byte[1024 * 1024 * 100];
        WeakReference<byte[]> weakReference = new WeakReference<>(bytes1);
        bytes1 = null;
        System.out.println(weakReference.get());
        // 告诉JVM进行回收
        System.gc();

        System.out.println(weakReference.get());
    }
}

直接运行,运行结果如下:
Alt text

6. 虚引用和终结器引用

这两种引用在常规开发中是不会使用的。
虚引用也叫幽灵引用/幻影引用,不能通过虚引用对象获取到包含的对象。虚引用唯一的用途是当对象被垃圾回 收器回收时可以接收到对应的通知。Java中使用PhantomReference实现了虚引用,直接内存中为了及时知道 直接内存对象不再使用,从而回收内存,使用了虚引用来实现。
比如我们之前使用的直接内存,JVM如何帮助我们释放的呢,查看源码:

java
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024 * 100);
buffer = null;

进入allocateDirect()方法,底层是使用DirectByteBuffer的构造函数:
Alt text 进入DirectByteBuffer的构造函数,Cleaner的create()方法传入当前的Buffer对象和线程类:
Alt text DirectByteBuffer使用Cleaner类用到虚引用PhantomReference:
Alt text 查看线程类Deallocator的run方法,当发现堆中ByteBuffer不再使用被回收了就会执行:
Alt text

虚引用存在的意义

有了虚引用就可以将堆内存的回收和直接内存的回收关联起来了

终结器引用指的是在对象需要被回收时,终结器引用会关联对象并放置在Finalizer类中的引用队列中,在稍后 由一条由FinalizerThread线程从队列中获取对象,然后执行对象的finalize方法,在对象第二次被回收时,该 对象才真正的被回收。在这个过程中可以在finalize方法中再将自身对象使用强引用关联上,但是不建议这样做。 finalize()方法是由JVM虚拟机进行调用,具有很大的不确定性,而且只会调用一次。

java
public class Jvm11 {
    static Jvm11 reference;
    public static void main(String[] args) throws InterruptedException {
        reference = new Jvm11();
        test();
        test();
    }

    public static void test() throws InterruptedException {
        reference = null;
        // 回收对象
        System.gc();

        // 由于finalize方法执行的线程优先级比较低,休眠500ms等待一下
        Thread.sleep(500);
        if(reference!=null){
            reference.alive();
        }else{
            System.out.println("对象已经被回收");
        }
    }

    @Override
    protected void finalize() throws Throwable {
        try {
            System.out.println("执行finalize方法。。。。。");
            reference = this;
        }finally {
            super.finalize();
        }
    }

    public void alive(){
        System.out.println("当前对象还活着");
    }
}

执行结果:
Alt text finalize()只会执行一次,所以第二次执行test()并不会触发reference的赋值。
Alt text

7. 总结

  • 强引用,最常见的引用方式,由可达性分析算法来判断
  • 软引用,对象在没有强引用情况下,内存不足时会回收
  • 弱引用,对象在没有强引用情况下,会直接回收
  • 虚引用,通过虚引用知道对象被回收了
  • 终结器引用,对象回收时可以自救,不建议使用