一次对ThreadLocal的学习引发的思考

ThreadLocal对Entry的引用是弱引用,于是联想到四种引用的生命周期。

  1. 强引用,不会进行垃圾回收
  2. 软引用,JVM内存不够,进行回收
  3. 弱引用,下次GC,直接进行回收
  4. 虚引用,不会对GC产生任何影响,结合ReferenceQueue使用,只为引用被回收时收到通知。

所以如果只有弱引用指向ThreadLocal,那么下次GC,ThreadLocal将被回收。阿里的代码规范上也要求ThreadLocal要被static修饰,就是为了防止后续还要使用,但ThreadLocal已被GC回收。

然后为了测试弱引用被GC写了下面的代码:

    @Test
    public void test() throws InterruptedException {
        ReferenceQueue queue = new ReferenceQueue();

        Thread thread = new Thread(() -> {
            while (true){
                Object obj;
                if((obj = queue.poll())!= null){
                        System.out.println("queue!!! " + obj);
                }
            }
        });

        thread.start();
        Reference reference1 = new Reference();
        WeakReference reference = new WeakReference(reference1, queue);
        System.out.println(reference);
        reference1 = null;

        System.gc();

        thread.join();

    }

    private static class Reference {

        @Override
        protected void finalize() throws Throwable {
            System.out.println("finalize!!!"+this);
        }
    }

输出如下:

java.lang.ref.WeakReference@200a570f
queue!!! java.lang.ref.WeakReference@200a570f
finalize!!!ConnectionTest$Reference@74560fd0

queue比finalize先输出,开始我以为是io竞争,忽视不理,将弱引用改为虚引用,这时输出为:

java.lang.ref.PhantomReference@200a570f
finalize!!!ConnectionTest$Reference@74560fd0

queue一直不输出,这时我怀疑是我代码问题,于是在网上百度了别人的例子改了下,可以正常输出queue,我又怀疑是我自定义类的问题,于是将原来代码虚引用改为String

PhantomReference reference = new PhantomReference(new String(), queue);

正常输出,what???但是我的自定义类没什么东西啊,只有一个finalize(),难道它会影响引用,但弱引用没问题,虚引用就有问题???于是我将虚引用改为软引用,然后创建大量的数组,正常输出。

于是我怀疑finalize对虚引用有什么影响,难道找到bug了,我百度了finalize和虚引用,然后找到以下文章:

JAVA虚引用为什么在重载finalize后不会在回收时被置入引用队列?

finalize方法执行过程

详细可以看:How to Handle Java Finalization's Memory-Retention Issues

  1. 重写了finalize()的类实例化时,JVM会标记该对象为finalizable
  2. GC thread检测到对象不可达时,如果对象是finalizable,会将对象添加到finalization queue,对象重新可达,推迟GC
  3. finalizer thread在一段时间之后,将会从finalization queue出队对象,调用对象的finalize(),随后标记对象为finalized
  4. GC thread重新检测到对象不可达,这时才回收对象。

看到这也就明白的虚引用在重写了finalize()之后为啥不输出queue,要经过两次GC,对象才会被回收,这时才进入Reference Queue,将代码改动如下:

    @Test
    public void test5() throws InterruptedException {
        Reference reference = new Reference();
        ReferenceQueue referenceQueue = new ReferenceQueue();
        Thread thread = new Thread(() -> {
            while (true){
                Object obj;
                if((obj = referenceQueue.poll())!= null){
                    System.out.println("queue!!! "+obj);
                }
            }
        });

        thread.start();
        PhantomReference reference1 = new PhantomReference(reference, referenceQueue);
        reference = null;
        System.gc();
        Thread.sleep(1000);
        System.gc();
        thread.join();
    }

输出如下:

finalize!!!ConnectionTest$Reference@74560fd0
queue!!! java.lang.ref.PhantomReference@5b708109

jstat也可以看到进行了两次的GC差别:

不输出queue的gc情况:

输出queue的gc情况:

虚引用和弱引用进入Reference Queue时机

上面已经解释了虚引用为啥不输出queue的原因,但为啥弱引用只经过一次gc就输出了queue的??

JAVA虚引用为什么在重载finalize后不会在回收时被置入引用队列?

弱引用、虚引用、finalize实践,及它们的顺序

这两篇文章提到:

  1. 弱引用:一旦对象只有弱引用,GC是会把弱引用直接插入引用队列,与插入finalization queue是同一时机。
  2. 虚引用:要在对象正式被回收,才进入引用队列

此外,我还发现jdk8虚引用不用调用clear()清除referent对象,要在引用队列中手动清除。

jdk8 PhantomReference注释如下:

02-13 15:47