彻底理解 java Reference

java引用体系中我们最熟悉的就是强引用类型,如 A a= new A();这是我们经常说的强引用StrongReference,jvm gc时会检测对象是否存在强引用,如果存在由根对象对其有传递的强引用,则不会对其进行回收,即使内存不足抛出OutOfMemoryError。

除了强引用外,Java还引入了SoftReference,WeakReference,PhantomReference,FinalReference ,这些类放在java.lang.ref包下,类的继承体系如下图


image.png

Java额外引入这个四种类型引用主要目的是在jvm 在gc时,按照引用类型的不同,在回收时采用不同的逻辑。可以把这些引用看作是对对象的一层包裹,jvm根据外层不同的包裹,对其包裹的对象采用不同的回收策略.

Reference指代引用对象本身,Referent指代被引用对象

对象可达性判断
jvm gc时,判断一个对象是否存在引用时,都是从根结合引用(Root Set of References)开始去标识,往往到达一个对象的引用路径会存在多条,如下图

image.png

那么 垃圾回收时会依据两个原则来判断对象的可达性:

  • 单一路径中,以最弱的引用为准
  • 多路径中,以最强的引用为准

例如Obj4的引用,存在3个路径:1->6、2->5、3->4, 那么从根对象到Obj4最强的引用是2->5,因为它们都是强引用。如果仅仅存在一个路径对Obj4有引用时,比如现在只剩1->6,那么根对象到Obj4的引用就是以最弱的为准,就是SoftReference引用,Obj4就是softly-reachable对象。

Java最初只有普通的强引用,只有对象存在引用,则对象就不会被回收,即使内存不足,也是如此,JVM会爆出OOM,也不会去回收存在引用的对象。

如果只提供强引用,我们就很难写出“这个对象不是很重要,如果内存不足GC回收掉也是可以的”这种语义的代码。Java在1.2版本中完善了引用体系,提供了4中引用类型:强引用,软引用,弱引用,虚引用。使用这些引用类型,我们不但可以控制垃圾回收器对对象的回收策略,同时还能在对象被回收后得到通知,进行相应的后续操作。

Java目前有4中引用类型:

  1. 强引用(Strong Reference):普通的的引用类型,new一个对象默认得到的引用就是强引用,只要对象存在强引用,就不会被GC。
  2. 软引用(Soft Reference):相对较弱的引用,垃圾回收器会在内存不足时回收弱引用指向的对象。JVM会在抛出OOME前清理所有弱引用指向的对象,如果清理完还是内存不足,才会抛出OOME。所以软引用一般用于实现内存敏感缓存。
  3. 弱引用(Weak Reference):更弱的引用类型,垃圾回收器在GC时会回收此对象,也可以用于实现缓存,比如JDK提供的WeakHashMap。
  4. 虚引用(Phantom Reference):一种特殊的引用类型,不能通过虚引用获取到关联对象,只是用于获取对象被回收的通知。

SoftReference:软引用,堆内存不足时,垃圾回收器会回收对应引用
WeakReference:弱引用,每次垃圾回收都会回收其引用
PhantomReference:虚引用,对引用无影响,只用于获取对象被回收的通知
FinalReference:Java用于实现finalization的一个内部类

Reference的核心

Java的多种引用类型实现,不是通过扩展语法实现的,而是利用类实现的,Reference类表示一个引用,其核心代码就是一个成员变量reference

public abstract class Reference {
    private T referent; // 会被GC特殊对待
    
    // 获取Reference管理的对象
    public T get() {
        return this.referent;
    }
    
    // ...
}

如果JVM没有对这个变量做特殊处理,它依然只是一个普通的强引用,之所以会出现不同的引用类型,是因为JVM垃圾回收器硬编码识别SoftReference,WeakReference,PhantomReference等这些具体的类,对其reference变量进行特殊对象,才有了不同的引用类型的效果。

Reference及其子类有两大功能:

  • 实现特定的引用类型
  • 用户可以对象被回收后得到通知

第一个功能很清楚,第二个功能是如何做到的呢?

一种思路是在新建一个Reference实例是,添加一个回调,当java.lang.ref.Reference#referent被回收时,JVM调用该回调,这种思路比较符合一般的通知模型,但是对于引用与垃圾回收这种底层场景来说,会导致实现复杂,性能不高的问题,比如需要考虑在什么线程中执行这个回调,回调执行阻塞怎么办等等。

所以Reference使用了一种更加原始的方式来做通知,就是把引用对象被回收的Reference添加到一个队列中,用户后续自己去从队列中获取并使用。

理解了设计后对应到代码上就好理解了,Reference有一个queue成员变量,用于存储引用对象被回收的Reference实例:

public abstract class Reference {
    // 会被GC特殊对待
    private T referent; 
    // reference被回收后,当前Reference实例会被添加到这个队列中
    volatile ReferenceQueue queue;
    
    // 只传入reference的构造函数,意味着用户只需要特殊的引用类型,不关心对象何时被GC
    Reference(T referent) {
        this(referent, null);
    }
    
    // 传入referent和ReferenceQueue的构造函数,reference被回收后,会添加到queue中
    Reference(T referent, ReferenceQueue queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }
    
    // ...
}

Reference的状态
Reference对象是有状态的。一共有4中状态:

  • Active:新创建的实例的状态,由垃圾回收器进行处理,如果实例的可达性处于合适的状态,垃圾回收器会切换实例的状态为Pending或者Inactive。如果Reference注册了ReferenceQueue,则会切换为Pending,并且Reference会加入pending-Reference链表中,如果没有注册ReferenceQueue,会切换为Inactive。
  • Pending:在pending-Reference链表中的Reference的状态,这些Reference等待被加入ReferenceQueue中。
  • Enqueued:在ReferenceQueue队列中的Reference的状态,如果Reference从队列中移除,会进入Inactive状态
  • Inactive:Reference的最终状态
image.png

除了上文提到的ReferenceQueue,这里出现了一个新的数据结构:pending-Reference。这个链表是用来干什么的呢?

上文提到了,reference引用的对象被回收后,该Reference实例会被添加到ReferenceQueue中,但是这个不是垃圾回收器来做的,这个操作还是有一定逻辑的。 如果垃圾回收器还需要执行这个操作,会降低其效率。从另外一方面想,Reference实例会被添加到ReferenceQueue中的实效性要求不高,所以也没必要在回收时立马加入ReferenceQueue。

所以垃圾回收器做的是一个更轻量级的操作:把Reference添加到pending-Reference链表中。Reference对象中有一个pending成员变量,是静态变量,它就是这个pending-Reference链表的头结点。要组成链表,还需要一个指针,指向下一个节点,这个对应的是java.lang.ref.Reference#discovered这个成员变量。

public abstract class Reference {
    // 会被GC特殊对待
    private T referent; 
    // reference被回收后,当前Reference实例会被添加到这个队列中
    volatile ReferenceQueue queue; 
    
    // 全局唯一的pending-Reference列表
    private static Reference pending = null;
    
    // Reference为Active:由垃圾回收器管理的已发现的引用列表(这个不在本文讨论访问内)
    // Reference为Pending:在pending列表中的下一个元素,如果没有为null
    // 其他状态:NULL
    transient private Reference discovered;  /* used by VM */
    // ...
}
 
 

ReferenceHandler线程
通过上文的讨论,我们知道一个Reference实例化后状态为Active,其引用的对象被回收后,垃圾回收器将其加入到pending-Reference链表,等待加入ReferenceQueue。这个过程是如何实现的呢?

这个过程不能对垃圾回收器产生影响,所以不能在垃圾回收线程中执行,也就需要一个独立的线程来负责。这个线程就是ReferenceHandler,它定义在Reference类中:

// 用于控制垃圾回收器操作与Pending状态的Reference入队操作不冲突执行的全局锁
// 垃圾回收器开始一轮垃圾回收前要获取此锁
// 所以所有占用这个锁的代码必须尽快完成,不能生成新对象,也不能调用用户代码
static private class Lock { };
private static Lock lock = new Lock();

private static class ReferenceHandler extends Thread {

    ReferenceHandler(ThreadGroup g, String name) {
        super(g, name);
    }

    public void run() {
        // 这个线程一直执行
        for (;;) {
            Reference r;
            // 获取锁,避免与垃圾回收器同时操作
            synchronized (lock) {
                // 判断pending-Reference链表是否有数据
                if (pending != null) {
                    // 如果有Pending Reference,从列表中取出
                    r = pending;
                    pending = r.discovered;
                    r.discovered = null;
                } else {
                    // 如果没有Pending Reference,调用wait等待
                    // 
                    // wait等待锁,是可能抛出OOME的,
                    // 因为可能发生InterruptedException异常,然后就需要实例化这个异常对象,
                    // 如果此时内存不足,就可能抛出OOME,所以这里需要捕获OutOfMemoryError,
                    // 避免因为OOME而导致ReferenceHandler进程静默退出
                    try {
                        try {
                            lock.wait();
                        } catch (OutOfMemoryError x) { }
                    } catch (InterruptedException x) { }
                    continue;
                }
            }

            // 如果Reference是Cleaner,调用其clean方法
            // 这与Cleaner机制有关系,不在此文的讨论访问
            if (r instanceof Cleaner) {
                ((Cleaner)r).clean();
                continue;
            }

            // 把Reference添加到关联的ReferenceQueue中
            // 如果Reference构造时没有关联ReferenceQueue,会关联ReferenceQueue.NULL,这里就不会进行入队操作了
            ReferenceQueue q = r.queue;
            if (q != ReferenceQueue.NULL) q.enqueue(r);
        }
    }
}
 
 

ReferenceHandler线程是在Reference的static块中启动的:

static {
    // 获取system ThreadGroup
    ThreadGroup tg = Thread.currentThread().getThreadGroup();
    for (ThreadGroup tgn = tg;
         tgn != null;
         tg = tgn, tgn = tg.getParent());
    Thread handler = new ReferenceHandler(tg, "Reference Handler");

    // ReferenceHandler线程有最高优先级
    handler.setPriority(Thread.MAX_PRIORITY);
    handler.setDaemon(true);
    handler.start();
}

综上,ReferenceHandler是一个最高优先级的线程,其逻辑是从Pending-Reference链表中取出Reference,添加到其关联的Reference-Queue中。

image.png

再来看些细节代码:

ReferenceQueue VS Reference

Reference作为SoftReference,WeakReference,PhantomReference,FinalReference这几个引用类型的父类。主要有两个字段referent、queue,一个是指所引用的对象,一个是与之对应的ReferenceQueue。Reference类有个构造函数 Reference(T referent, ReferenceQueue queue),可以通过该构造函数传入与Reference相伴的ReferenceQueue。

ReferenceQueue本身提供队列的功能,有入队(enqueue)和出队(poll,remove,其中remove阻塞等待提取队列元素)。ReferenceQueue对象本身保存了一个Reference类型的head节点,Reference封装了next字段,这样就是可以组成一个单向链表。同时ReferenceQueue提供了两个静态字段NULL,ENQUEUED

static ReferenceQueue NULL = new Null<>();
static ReferenceQueue ENQUEUED = new Null<>();
 
 

这两个字段的主要功能:NULL是当我们构造Reference实例时queue传入null时,会默认使用NULL,这样在enqueue时判断queue是否为NULL,如果为NULL直接返回,入队失败。ENQUEUED的作用是防止重复入队,reference后会把其queue字段赋值为ENQUEUED,当再次入队时会直接返回失败。

 boolean enqueue(Reference r) { /* Called only by Reference class */
        synchronized (lock) {
            // Check that since getting the lock this reference hasn't already been
            // enqueued (and even then removed)
            ReferenceQueue queue = r.queue;
            if ((queue == NULL) || (queue == ENQUEUED)) {
                return false;
            }
            assert queue == this;
            r.queue = ENQUEUED;
            r.next = (head == null) ? r : head;
            head = r;
            queueLength++;
            if (r instanceof FinalReference) {
                sun.misc.VM.addFinalRefCount(1);
            }
            lock.notifyAll();
            return true;
        }
    }

Reference与ReferenceQueue之间是如何工作的呢?Reference里有个静态字段pending,同时还通过静态代码块启动了Reference-handler thread。当一个Reference的referent被回收时, 垃圾回收器会把reference添加到pending这个链表里,然后Reference-handler thread不断的读取pending中的reference,把它加入到对应的ReferenceQueue中, 我们可以通过下面代码块来进行把SoftReference,WeakReference,PhantomReference与ReferenceQueue联合使用来验证这个机制。为了确保SoftReference在每次gc后,其引用的referent都被回收,我们需要加入-XX:SoftRefLRUPolicyMSPerMB=0参数,

通过jstack命令可以看到对应的Reference Handler thread

"Reference Handler" #2 daemon prio=10 os_prio=31 tid=0x00007f8fb2836800 nid=0x2e03 in Object.wait() [0x000070000082b000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x0000000740008878> (a java.lang.ref.Reference$Lock)
        at java.lang.Object.wait(Object.java:502)
        at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
        - locked <0x0000000740008878> (a java.lang.ref.Reference$Lock)
        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

因此可以看出,当reference与referenQueue联合使用的主要作用就是当reference指向的referent回收时(或者要被回收 如下文要讲的Finalizer),提供一种通知机制,通过queue取到这些reference,来做额外的处理工作。当然,如果我们不需要这种通知机制,我们就不用传入额外的queue,默认使用NULL queue就会入队失败。

SoftReference

根据上面我们讲的对象可达性原理,我们把一个对象存在根对象对其有直接或间接的SoftReference,并没有其他强引用路径,我们把该对象成为softly-reachable对象。JVM保证在抛出OutOfMemoryError前会回收这些softly-reachable对象。JVM会根据当前内存的情况来决定是否回收softly-reachable对象,但只要referent有强引用存在,该referent就一定不会被清理,因此SoftReference适合用来实现memory-sensitive caches。软引用的回收策略在不同的JVM实现会略有不同,javadoc中说明:

Virtual machine implementations are, however, encouraged to bias against clearing recently-created or recently-used soft references.

也就是说JVM不仅仅只会考虑当前内存情况,还会考虑软引用所指向的referent最近使用情况和创建时间来综合决定是否回收该referent。

Hotspot在gc时会根据两个标准来回收:

  • 根据SoftReference引用实例的timestamp(每次调用softReference.get()会自动更新该字段,把最近一次垃圾回收时间赋值给timestamp,见源码)
  • 当前JVM heap的内存剩余(free_heap)情况

计算的规则是:

  • free_heap 表示当前堆剩余的内存,单位是MB
  • interval 表示最近一次GC's clock 和 当前我们要判断的softReference的timestamp 差值
  • ms_per_mb is a constant number of milliseconds to keep around a SoftReference for each free megabyte in the heap(可以通过-XX:SoftRefLRUPolicyMSPerMB来设定)

那么判断依据就是: interval <= free_heap * ms_per_mb,如果为true,则保留,false则进行对象清除。_ ** SoftReferences will always be kept for at least one GC after their last access。**_ 因为 只要调用一次,那么clock和timestamp的值就会一样,clock-timestamp则为0,一定小于等于free_heap * ms_per_mb。 OpenJDK的大概referencePolicy.cpp代码是:

void LRUMaxHeapPolicy::setup() {
  size_t max_heap = MaxHeapSize;
  max_heap -= Universe::get_heap_used_at_last_gc();
  max_heap /= M;

  _max_interval = max_heap * SoftRefLRUPolicyMSPerMB;
  assert(_max_interval >= 0,"Sanity check");
}

bool LRUMaxHeapPolicy::should_clear_reference(oop p,
                                             jlong timestamp_clock) {
  jlong interval = timestamp_clock - java_lang_ref_SoftReference::timestamp(p);
  assert(interval >= 0, "Sanity check");

  // The interval will be zero if the ref was accessed since the last scavenge/gc.
  if(interval <= _max_interval) {
    return false;
  }

  return true;
}

可见,SoftReference在一定程度上会影响JVM GC的,例如softly-reachable对应的referent多次垃圾回收仍然不满足释放条件,那么它会停留在heap old区,占据很大部分空间,在JVM没有抛出OutOfMemoryError前,它有可能会导致频繁的Full GC。

WeakReference

当一个对象被WeakReference引用时,处于weakly-reachable状态时,只要发生GC时,就会被清除,同时会把WeakReference注册到引用队列中(如果存在的话)。 WeakReference不阻碍或影响它们对应的referent被终结(finalized)和回收(reclaimed),因此,WeakReference经常被用作实现规范映射(canonicalizing mappings)。相比SoftReference来说,WeakReference对JVM GC几乎是没有影响的。

下面我们举个WeakReference应用场景,JDK自带的WeakHashMap,我们用下面的代码来测试查看WeakHashMap在gc后的entry的情况,加入-verbose:gc运行。

/**
 * 加入下面参数,观察gc情况
 * -verbose:gc
 */
public class WeakHashMapTest {

    private static Map caches=new WeakHashMap<>();

    public static void main(String[]args) throws InterruptedException {
        for (int i=0;i<100000;i++){
            caches.put(i+"",new byte[1024*1024*10]);
            System.out.println("put num: " + i + " but caches size:" + caches.size());
        }
    }
}

运行代码我们可以看到,虽然我们不断的往caches中put元素,但是caches size会伴随每次gc又从0开始了。

WeakHashMap实现原理很简单,它除了实现标准的Map接口,里面的机制也和HashMap的实现类似。从它entry子类中可以看出,它的key是用WeakReference包裹住的。当这个key对象本身不再被使用时,伴随着GC的发生,会自动把该key对应的entry都在Map中清除掉。它为啥能够自动清除呢?这就是利用上面我们讲的ReferenceQueue VS Reference的原理。WeakHashMap里声明了一个queue,Entry继承WeakReference,构造函数中用key和queue关联构造一个weakReference,当key不再被使用gc后会自动把把key注册到queue中:

 /**
     * Reference queue for cleared WeakEntries
     */
    private final ReferenceQueue queue = new ReferenceQueue<>();

   /**
     * The entries in this hash table extend WeakReference, using its main ref
     * field as the key.
     */
    private static class Entry extends WeakReference implements Map.Entry {
        V value;
        final int hash;
        Entry next;

        /**
         * Creates new entry.
         */
        Entry(Object key, V value,
              ReferenceQueue queue,
              int hash, Entry next) {
            super(key, queue);
            this.value = value;
            this.hash  = hash;
            this.next  = next;
        }
       //代码省略
    }
}
 
 

WeakHashMap关键的清理entry代码:

/**
     * Expunges stale entries from the table.
     */
    private void expungeStaleEntries() {
        for (Object x; (x = queue.poll()) != null; ) {
            synchronized (queue) {
                @SuppressWarnings("unchecked")
                    Entry e = (Entry) x;
                int i = indexFor(e.hash, table.length);

                Entry prev = table[i];
                Entry p = prev;
                while (p != null) {
                    Entry next = p.next;
                    if (p == e) {
                        if (prev == e)
                            table[i] = next;
                        else
                            prev.next = next;
                        // Must not null out e.next;
                        // stale entries may be in use by a HashIterator
                        e.value = null; // Help GC
                        size--;
                        break;
                    }
                    prev = p;
                    p = next;
                }
            }
        }
    }

这段代码会在resize,getTable,size里执行,清除失效的entry。

PhantomReference

PhantomReference 不同于WeakReference、SoftReference,它存在的意义不是为了获取referent,因为你也永远获取不到,因为它的get如下

public T get() {
        return null;
 }

PhantomReference主要作为其指向的referent被回收时的一种通知机制,它就是利用上文讲到的ReferenceQueue实现的。当referent被gc回收时,JVM自动把PhantomReference对象(reference)本身加入到ReferenceQueue中,像发出信号通知一样,表明该reference指向的referent被回收。然后可以通过去queue中取到reference,此时说明其指向的referent已经被回收,可以通过这个通知机制来做额外的清场工作。 因此有些情况可以用PhantomReference 代替finalize(),做资源释放更明智。

下面举个例子,用PhantomReference来自动关闭文件流。

public class ResourcePhantomReference extends PhantomReference {

    private List closeables;

    public ResourcePhantomReference(T referent, ReferenceQueue q, List resource) {
        super(referent, q);
        closeables = resource;
    }

    public void cleanUp() {
        if (closeables == null || closeables.size() == 0)
            return;
        for (Closeable closeable : closeables) {
            try {
                closeable.close();
                System.out.println("clean up:"+closeable);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
public class ResourceCloseDeamon extends Thread {

    private static ReferenceQueue QUEUE = new ReferenceQueue();

    //保持对reference的引用,防止reference本身被回收
    private static List references=new ArrayList<>();
    @Override
    public void run() {
        this.setName("ResourceCloseDeamon");
        while (true) {
            try {
                ResourcePhantomReference reference = (ResourcePhantomReference) QUEUE.remove();
                reference.cleanUp();
                references.remove(reference);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void register(Object referent, List closeables) {
        references.add(new ResourcePhantomReference(referent,QUEUE,closeables));
    }


}
public class FileOperation {

    private FileOutputStream outputStream;

    private FileInputStream inputStream;

    public FileOperation(FileInputStream inputStream, FileOutputStream outputStream) {
        this.outputStream = outputStream;
        this.inputStream = inputStream;
    }

    public void operate() {
        try {
            inputStream.getChannel().transferTo(0, inputStream.getChannel().size(), outputStream.getChannel());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


}

public class PhantomTest {

    public static void main(String[] args) throws Exception {
        //打开回收
        ResourceCloseDeamon deamon = new ResourceCloseDeamon();
        deamon.setDaemon(true);
        deamon.start();

        // touch a.txt b.txt
        // echo "hello" > a.txt

        //保留对象,防止gc把stream回收掉,其不到演示效果
        List all=new ArrayList<>();
        FileInputStream inputStream;
        FileOutputStream outputStream;

        for (int i = 0; i < 100000; i++) {
            inputStream = new FileInputStream("/Users/robin/a.txt");
            outputStream = new FileOutputStream("/Users/robin/b.txt");
            FileOperation operation = new FileOperation(inputStream, outputStream);
            operation.operate();
            TimeUnit.MILLISECONDS.sleep(100);

            Listcloseables=new ArrayList<>();
            closeables.add(inputStream);
            closeables.add(outputStream);
            all.addAll(closeables);
            ResourceCloseDeamon.register(operation,closeables);
            //用下面命令查看文件句柄,如果把上面register注释掉,就会发现句柄数量不断上升
            //jps | grep PhantomTest | awk '{print $1}' |head -1 | xargs  lsof -p  | grep /User/robin
            System.gc();

        }


    }

运行上面的代码,通过jps | grep PhantomTest | awk '{print $1}' |head -1 | xargs lsof -p | grep /User/robin | wc -l 可以看到句柄没有上升,而去掉ResourceCloseDeamon.register(operation,closeables);时,句柄就不会被释放。

PhantomReference使用时一定要传一个referenceQueue,当然也可以传null,但是这样就毫无意义了。因为PhantomReference的get结果为null,如果在把queue设为null,那么在其指向的referent被回收时,reference本身将永远不会可能被加入队列中.

FinalReference

FinalReference 引用类型主要是为虚拟机提供的,提供 _ 对象被gc前需要执行finalize方法的对象_ 的机制。

FinalReference 很简单就是extend Reference类,没有做其他逻辑,只是把访问权限改为package,因此我们是无法直接使用的。Finalizer类是我们要讲的重点,它继承了FinalReference,并且是final 类型的。Finalize实现很简单,也是利用上面我们讲的ReferenceQueue VS Reference机制。

FinalizerThread

Finalizer静态代码块里启动了一个deamon线程,我们通过jstack命令查看线程时,总会看到一个Finalizer线程,就是这个原因:

 static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        Thread finalizer = new FinalizerThread(tg);
        finalizer.setPriority(Thread.MAX_PRIORITY - 2);
        finalizer.setDaemon(true);
        finalizer.start();
    }

FinalizerThread run方法是不断的从queue中去取Finalizer类型的reference,然后执行runFinalizer释放方法。

 public void run() {
            if (running)
                return;

            // Finalizer thread starts before System.initializeSystemClass
            // is called.  Wait until JavaLangAccess is available
            while (!VM.isBooted()) {
                // delay until VM completes initialization
                try {
                    VM.awaitBooted();
                } catch (InterruptedException x) {
                    // ignore and continue
                }
            }
            final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
            running = true;
            for (;;) {
                try {
                    Finalizer f = (Finalizer)queue.remove();
                    f.runFinalizer(jla);
                } catch (InterruptedException x) {
                    // ignore and continue
                }
            }
       }

runFinalizer方法体,执行事发逻辑,可以看出如果finalize方法中抛出异常会被直接吃掉:

  private void runFinalizer(JavaLangAccess jla) {
        synchronized (this) {
            if (hasBeenFinalized()) return;
            remove();
        }
        try {
            Object finalizee = this.get();
            if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
                jla.invokeFinalize(finalizee);

                /* Clear stack slot containing this variable, to decrease
                   the chances of false retention with a conservative GC */
                finalizee = null;
            }
        } catch (Throwable x) { }
        super.clear();
    }

介绍完上面的处理机制,那么剩下的就是入queue的事情,就是哪些类对象需要入队,何时入队.

哪些类对象是Finalizer reference类型的referent呢

只要类覆写了Object 上的finalize方法,方法体非空。那么这个类的实例都会被Finalizer引用类型引用的。下文中我们简称Finalizer 型的referent为finalizee。

何时调用Finalizer.register生成一个Finalizer类型的reference

Finalizer的构造函数是private的,也就是不能通过new 来生成一个Fianlizer reference。只能通过静态的register方法来生成。同时Finalizer有个静态字段unfinalized,维护了一个未执行finalize方法的reference列表,在构造函数中通过add()方法把Finalizer引用本身加入到unfinalized列表中,同时关联finalizee和queue,实现通知机制。维护静态字段unfinalized的目的是为了一直保持对未未执行finalize方法的reference的强引用,防止被gc回收掉。

  private static Finalizer unfinalized = null;
    private Finalizer(Object finalizee) {
        super(finalizee, queue);
        add();
    }
    /* Invoked by VM */
    static void register(Object finalizee) {
        new Finalizer(finalizee);
    }
    private void add() {
        synchronized (lock) {
            if (unfinalized != null) {
                this.next = unfinalized;
                unfinalized.prev = this;
            }
            unfinalized = this;
        }
    }

那么register是被VM何时调用的呢?JVM通过VM参数 RegisterFinalizersAtInit 的值来确定何时调用register,RegisterFinalizersAtInit默认为true,则会在构造函数返回之前调用call_register_finalizer方法。

void Parse::return_current(Node* value) {
  if (RegisterFinalizersAtInit &&
      method()->intrinsic_id() == vmIntrinsics::_Object_init) {
    call_register_finalizer();
  }
  ..............
}

如果通过-XX:-RegisterFinalizersAtInit 设为false,则会在对象空间分配好之后就调用call_register_finalizer

nstanceOop InstanceKlass::allocate_instance(TRAPS) {
  bool has_finalizer_flag = has_finalizer(); // Query before possible GC
  int size = size_helper();  // Query before forming handle.

  KlassHandle h_k(THREAD, this);

  instanceOop i;

  i = (instanceOop)CollectedHeap::obj_allocate(h_k, size, CHECK_NULL);
  if (has_finalizer_flag && !RegisterFinalizersAtInit) {
    i = register_finalizer(i, CHECK_NULL);
  }
  return i;
}

何时入queue
当一个finalizee 只剩Finalizer引用,没有其他引用时,需要被回收了,GC就会把该finalizee对应的reference放到Finalizer的refereneQueue中,等待FinalizerThread来执行finalizee的finalize方法,然后finalizee对象才能被GC回收。

Finalizer问题

  1. finalizee对象在finalize重新被赋给一个强引用复活,那么下次GC前会不会被再次执行finalize方法?

答案是不会的,runFinalizer中会把该finalizee对应的Finalizer引用从unfinalized队列中移除,第二次执行的时会通过hasBeenFinalized方法判断,保证不会被重复执行。

 private void runFinalizer(JavaLangAccess jla) {
        synchronized (this) {
            if (hasBeenFinalized()) return;
            remove();
        }
        try {
            Object finalizee = this.get();
            if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
                jla.invokeFinalize(finalizee);

                /* Clear stack slot containing this variable, to decrease
                   the chances of false retention with a conservative GC */
                finalizee = null;
            }
        } catch (Throwable x) { }
        super.clear();
    }
  1. finalizee至少两次GC回收才可能被回收?
    第一次GC把finalizee对应的Finalizer reference加入referenceQueue等待FinalizerThread来执行finalize方法。第二次GC才有可能释放finalizee对象本身,前提是FinalizerThread已经执行完finalize方法了,并把Finalizer reference从Finalizer静态unfinalized链表中剔除,因为这个链表和Finalizer reference对finalizee构成的是一个强引用。

  2. Finalizer 机制导致JVM Full GC 频繁,stop-the-world延长?

因为如果finalizee上的finalize方法体执行过程耗时比较长,会导致对象一直堆积,多次GC仍不能释放,冲进old区,造成Old区GC过程延长,暂停时间增加,可能频繁触发Full GC。

小结

通过对SoftReference,WeakReference,PhantomReference,FinalReference 的介绍,可以看出JDK提供这些类型的reference 主要是用来和GC交互的,根据reference的不同,让JVM采用不同策略来进行对对象的回收(reclaim)。softly-reachable的referent在保证在OutOfMemoryError之前回收对象,weakly-reachable的referent在发生GC时就会被回收,finalizer型的reference 主要提供GC前对referent进行finalize执行机制。同时这些reference和referenceQueue在一起提供通知机制,PhantomReference的作用就是仅仅就是提供对象回收通知机制,Finalizer借助这种机制实现referent的finalize执行,SoftReference、WeakReference也可以配合referenceQueue使用,实现对象回收通知机制。

你可能感兴趣的:(彻底理解 java Reference)