澳门新浦京8455comReference

澳门新浦京8455com 6

前面把基于特定数据结构的Map介绍完了,它们分别利用了相应数据结构的特点来实现特殊的目的,像HashMap利用哈希表的快速插入、查找实现O(1)的增删改查,TreeMap则利用了红黑树来保证key的有序性的同时,使得增删改查的时间复杂度为O(log(n))

概述

  在学习WeakHashMap之前,先简单来说一下Java中的4种引用类型,它们分别是:强引用(Strong
Reference),软引用(Soft Reference),弱引用(Weak
Reference),幽灵引用或者翻译为虚引用(Phantom Reference)。

  1. 强引用:强引用是Java中最普遍的应用,比如new
    Object,新建的object对象就属于强引用类型。如果一个对象是强引用类型,那么即使是Java虚拟机内存空间不足时,GC也不会回收该对象,而是内存溢出,比如我们常见的OutOfMemoryError错误。
  2. 软引用:软引用是强度仅次于强引用的一种类型,它使用类SoftReference来表示。当虚拟机内存足够时,是不会回收这些软引用对象的。而当虚拟机内存不足时,GC会回收那些被软引用指向的对象。如果释放完这些对象后,虚拟机仍然内存不足,这时候才会抛出OutOfMemoryError错误。所以说软引用适合用于创建缓存,因为缓存中的对象相比其他对象,在内存不足的时候是可以释放掉的,而Mybatis中就有它的身影。
  3. 弱引用:弱引用在强度上又弱于软引用,它使用类WeakReference来表示。它相比软引用而言,拥有更短暂的生命周期。它可以引用一个对象,但并不阻止该对象被GC回收。在垃圾回收的时候,不管内存是否充足,如果一个对象的所有引用都是弱引用,那么该对象就会被回收。所以说,弱引用的对象的生命周期是两次GC之间的这段时间,也就是说其生命周期只存在于一个垃圾回收周期内,只能存活到下次GC之前;
  4. 幽灵引用:虚引用,形同虚设的引用,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象是虚引用了对象,那么这个引用有和没有是差不多的,在任何时候都可以被垃圾回收器回收。而虚引用主要用来跟踪对象被垃圾回收器回收的过程的,比如说程序可以在确定一个对象要被回收之后,再申请内存创建新的对象。通过这种方式可以使得程序所消耗的内存维持在一个相对较低的数量。虚引用必须和引用队列一起使用。
  5. 引用队列:引用队列是一种属于监听性质的结构。比如说,一个对象的状态发生了变化,从强引用变为了弱引用,而引用队列就是用于获取这些引用信息的队列,并在合适的时候对这些引用做处理。

  简单说了下Java中的4种引用,因为这不是本篇文章的重点,等以后有时间了再仔细研究一下这几种引用。现在我们开始学习WeakHashMap。

WeakHashMap是一种基于Java的弱引用的哈希表实现。它的目的和常规的Map实现有些不同,它主要是用于优化JVM,使JVM在进行垃圾回收的时候能智能的回收那些无用的对象。

参考自:http://www.androidstar.cn/java%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B%E4%B9%8Breference%E8%AF%A6%E8%A7%A3/
JAVA引用体系中我们最熟悉的就是强引用类型,如 A a= new
A();这是我们经常说的强引用StrongReference,jvm
gc时会检测对象是否存在强引用,如果存在由根对象对其有传递的强引用,则不会对其进行回收,即使内存不足抛出OutOfMemoryError。

今天要介绍的WeakHashMap并没有基于某种特殊的数据结构,它的主要目的是为了优化JVM,使JVM中的垃圾回收器(garbage
collector,后面简写为 GC)更智能的回收“无用”的对象。

属性

public class WeakHashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V> {

  其实WeakHashMap中的继承体系和大部分常量都和HashMap没什么不同。在存储上的不同点,或许是WeakHashMap解决index冲突仍旧使用的是链表,并没有使用红黑树。大概有以下特性:

  1. 根据API文档,当Map中的键不再使用,键对应的键值也将自动在WeakHashMap中删除。WeakHashMap中的键为弱键,和其他Map接口的实现有些不同;
  2. 和HashMap类似,支持key和value为null;
  3. 同样不是线程安全的,可以使用Collections.synchronizedMap来使之线程安全;
  4. 没有实现Cloneable, Serializable接口;
// 比HashMap少了一些属性,但多了一个弱键的引用队列
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();

  该引用队列,用于存放虚拟机回收的Entry的引用,也就是说,一旦GC之后有key被清除,那key对应的引用就会被放入引用队列中。

大家可以看下静态内部类Entry:

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
    V value;
    final int hash;
    Entry<K,V> next;
    Entry(Object key, V value,
          // 关联引用队列
          ReferenceQueue<Object> queue,
          int hash, Entry<K,V> next) {
        super(key, queue);
        this.value = value;
        this.hash  = hash;
        this.next  = next;
    }
}

  大家可以看到,Entry继承了WeakReference,所以Entry是个弱引用类型。Entry生成的时候就将与ReferenceQueue绑定,这样我们就可以实现对弱引用的监听,一旦JVM回收后,那么对应的引用就会加入到引用队列中。

除了强引用外,Java还引入了SoftReference,WeakReference,PhantomReference,FinalReference
,这些类放在java.lang.ref包下,类的继承体系如下图。Java额外引入这个四种类型引用主要目的是在jvm
在gc时,按照引用类型的不同,在回收时采用不同的逻辑
。可以把这些引用看作是对对象的一层包裹,jvm根据外层不同的包裹,对其包裹的对象采用不同的回收策略,或特殊逻辑处理。
这几种类型的引用主要在jvm内存缓存、资源释放、对象可达性事件处理等场景会用到

引用类型

WeakHashMap与其他 Map 最主要的不同之处在于其 key 是弱引用类型,其他
Map 的 key 均为强引用类型,说到这里,必须强调下:Java
中,引用有四种类型,分别为:强(strong)引用、软(soft)引用、弱(weak)引用、虚(phantom,本意为幽灵)引用。我相信对于
Java 初学者来说,不一定听过这几种引用类似,下面先介绍下这几种类型。

方法

  WeakHashMap中的大部分方法都和HashMap类似,由于没有红黑树的存在,大部分方法还是挺简单的,今天主要来看expungeStaleEntries这个方法,也就是WeakHashMap弱引用实现的关键方法。

/**
 * expungeStaleEntries方法就是在引用队列中寻找是否有被回收的key的引用,
 * 如果有,则在table数组中删掉其对应的映射。
 */
private void expungeStaleEntries() {
    // 遍历队列,通过队列的poll方法从队头获取数据,如果存在被GC的对象,就需要移除map中对应的数据
    for (Object x; (x = queue.poll()) != null; ) {
        // 线程同步该队列
        synchronized (queue) {
            @SuppressWarnings("unchecked")
                // 队列中保存的就是Entry
                Entry<K,V> e = (Entry<K,V>) x;
            // 获取当前节点的索引位置
            int i = indexFor(e.hash, table.length);
            // 获取索引位置的节点
            Entry<K,V> prev = table[i];
            Entry<K,V> p = prev;
            // 判断节点是否存在
            while (p != null) {
                // p的下个节点
                Entry<K,V> next = p.next;
                // 如果p就是当前节点
                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
                    // 将value设为null,帮助GC回收
                    e.value = null; // Help GC
                    size--;
                    break;
                }
                prev = p;
                p = next;
            }
        }
    }
}

其中上述循环语句简单说两句:

  1. 获取到索引所在的链表。遍历链表;
  2. 如果这个链表的头节点就是当前节点,那么就把链表的下一个节点移到头节点,然后设置value为null,进行后续操作;
  3. 如果链表头节点不是当前节点,后续根据next进行遍历,挨个判断。如果查询到当前节点,设置value为null,进行后续操作。

  在WeakHashMap中,大部分方法都会直接或间接调用该方法,来进行清除已经被回收的key的映射操作。

现在我们可以总结一下WeakhashMap弱引用的大概原理了。

  1. 创建WeakHashMap,添加对应的键值对信息,而底层是使用一个数组来保存对应的键值对信息Entry,而Entry生成的时候就与引用队列ReferenceQueue进行了关联;
  2. 当某弱键key不再被其他对象使用,并被JVM回收时,这个弱键对应的Entry会被同时添加到引用队列中去。
  3. 当下一次我们操作WeakHashMap时(比如调用get方法),会先处理引用队列中的这部分数据,这样这些弱键值对就自动在WeakHashMap中被自动删除了。

那么,其实还有另一个问题:被GC清除后的引用是什么时候进入引用队列的呢。

澳门新浦京8455com 1

强引用

这是最常用的引用类型,在执行下面的语句时,变量 o 即为一个强引用。

Object o = new Object();

强引用指向的对象无论在何时,都不会被GC 清理掉。

一般来说,对于常驻类应用(比如server),随着时间的增加,所占用的内存往往会持续上升,如果程序中全部使用强引用,那么很容易造成内存泄漏,最终导致Out Of Memory (OOM),所以
Java
中提供了除强引用之外的其他三种引用,它们全部位于java.lang.ref包中,下面一一介绍。

ReferenceHandler线程

我们可以通过Entry的结构看到,Entry是继承自WeakReference,而WeakReference是继承自Reference。
我们从Entry的构造方法开始看:

public WeakReference(T referent, ReferenceQueue<? super T> q) {
    super(referent, q);
}
Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

可以看到,最终是进入了Reference抽象类。

  通过阅读Reference的文档,我们知道Reference对象是与垃圾回收器有直接的关联的。而这种直接的关联是通过ReferenceHandler
这个线程来实现的。ReferenceHandler线程是JVM创建main线程后创建的线程,其优先级最高,是10,它就是用来处理引用对象的垃圾回收问题的。

我们来介绍下Reference的一些变量和方法。

public abstract class Reference<T> {

    // GC线程在回收对象的时候的锁
    static private class Lock { }
    private static Lock lock = new Lock();
    // 存放被回收的引用对象,该对象受上面锁的保护
    private static Reference<Object> pending = null;
    // 引用队列,存放pending
    volatile ReferenceQueue<? super T> queue;

    // 静态内部类,ReferenceHandler线程,处理引用队列的高线程。
    // 在static块里面被初始化。该守护线程启动后,会处于等待状态,
    private static class ReferenceHandler extends Thread {
    }
}

我们可以大概看一下JVM进行GC时ReferenceHandler线程所做的工作:

  1. JVM在进行GC的时候,会创建ConcurrentMarkSweepThread线程(简称CMST)去执行GC,并且同时创建SurrogateLockerThread线程(简称SLT)。CMST开始GC时,会发一个消息给SLT让它去获取Java的Reference对象的全局锁:Lock。直到CMS
    GC完毕之后,JVM会将WeakHashMap中所有被回收的对象所属的WeakReference容器对象放入到Reference的pending属性当中,然后通知SLT释放并且notify全局锁:Lock。此时激活了ReferenceHandler线程的run方法,使其脱离wait状态,开始工作了。
  2. ReferenceHandler这个线程会将pending中的所有WeakReference对象都移动到它们各自的列队当中,比如当前这个WeakReference属于某个WeakHashMap对象,那么它就会被放入相应的ReferenceQueue列队里面(该列队是链表结构)。
  3. 然后当我们操作WeakHashMap的时候,就会相应的处理引用队列中的这部分数据。

我们来看一下Reference中的静态代码块:

static {
    // 获取线程组
    ThreadGroup tg = Thread.currentThread().getThreadGroup();
    for (ThreadGroup tgn = tg;
         tgn != null;
         tg = tgn, tgn = tg.getParent());
    // 然后创建ReferenceHandler线程对象
    Thread handler = new ReferenceHandler(tg, "Reference Handler");
    /* If there were a special system-only priority greater than
     * MAX_PRIORITY, it would be used here
     */
    // 设置最高优先级
    handler.setPriority(Thread.MAX_PRIORITY);
    // 设置守护线程
    handler.setDaemon(true);
    // 守护线程启动
    handler.start();

    // provide access in SharedSecrets
    SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
        @Override
        public boolean tryHandlePendingReference() {
            return tryHandlePending(false);
        }
    });
}

而ReferenceHandler中重载的run方法如下:

public void run() {
    while (true) {
        tryHandlePending(true);
    }
}
static boolean tryHandlePending(boolean waitForNotify) {
    // pending这里,在GC的时候,JVM在通过计算对象key的可达性后,发现没有该key对象的引用,就会把该对象关联的Entry添加到pending中。
// pending这里会涉及到线程的阻塞,如果pending为空,会阻塞当前线程
    Reference<Object> r;
    Cleaner c;
    try {
        synchronized (lock) {
            if (pending != null) {
                r = pending;
                c = r instanceof Cleaner ? (Cleaner) r : null;
                // unlink 'r' from 'pending' chain
                pending = r.discovered;
                r.discovered = null;
            } else {
                if (waitForNotify) {
                    lock.wait();
                }
                // retry if waited
                return waitForNotify;
            }
        }
    } catch (OutOfMemoryError x) {
        Thread.yield();
        // retry
        return true;
    } catch (InterruptedException x) {
        // retry
        return true;
    }

    // Fast path for cleaners
    if (c != null) {
        c.clean();
        return true;
    }
    // 将pending放入引用队列中
    ReferenceQueue<? super Object> q = r.queue;
    if (q != ReferenceQueue.NULL) q.enqueue(r);
    return true;
}

而到这里,我们也基本上明白了弱引用对象是通过什么方式进入引用队列的了。

java_Reference

java.lang.ref.Reference

java.lang.ref.Reference 为
软(soft)引用、弱(weak)引用、虚(phantom)引用的父类。

澳门新浦京8455com 2

Reference类继承关系

下面分析下Reference的源码(其他三种引用都是其子类,区分不是很大)。

例子

我们通过一个简单的例子来看一下WeakHashMap的实现:

public static void main(String[] args) {
    Map<String, String> weakMap = new WeakHashMap<>();
    weakMap.put(new String("1"), "1");
    weakMap.put(new String("2"), "2");
    weakMap.put(new String("3"), "3");
    weakMap.put("4", "4");
    String five = new String("5");
    weakMap.put(five, "5");
    System.out.println("weakMap.size:" + weakMap.size());
    //手动触发 GC
    System.gc();
    try {
        Thread.sleep(50);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("=============");
    System.out.println("weakMap:" + weakMap);
    System.out.println("weakMap.size:" + weakMap.size());
}

总共放入Map中5个对象,我们运行一下结果:

weakMap.size:5
=============
weakMap:{4=4, 5=5}
weakMap.size:2

可以看到,Map里只剩下后两个对象了。接下来,我们稍微改动下代码:

// 我们在put第5个对象之后,将five设置为null,再看下打印结果
weakMap.put(five, "5");
five = null;

weakMap.size:5
=============
weakMap:{4=4}
weakMap.size:1

可以看到,第5个对象也被回收掉了。
从上面可以知道,数据1,2,3因为没有其他的引用,所以会被GC进行回收,而数据4是常量,是放在常量池中的,一般是不会被GC进行回收的。对于数据5,因为指向的引用为null了,所以被回收了。

名称说明下:Reference指代引用对象本身,Referent指代被引用对象,下文介绍会以Reference,Referent形式出现。
下面我们先介绍一下Java对象可达性判断逻辑和ReferenceQueue,然后依次对这四种引用使用和作用进行说明。

构造函数

//referent 为引用指向的对象
Reference(T referent) {
    this(referent, null);
}
//ReferenceQueue对象,可以简单理解为一个队列
//GC 在检测到appropriate reachability changes之后,
//会把引用对象本身添加到这个queue中,便于清理引用对象本身
Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

如果我们在创建一个引用对象时,指定了ReferenceQueue,那么当引用对象指向的对象达到合适的状态(根据引用类型不同而不同)时,GC
会把引用对象本身添加到这个队列中,方便我们处理它,因为

引用对象指向的对象 GC
会自动清理,但是引用对象本身也是对象(是对象就占用一定资源),所以需要我们自己清理。

举个例子:

SoftReference<String> ss = new SoftReference<String>("abc" , queue);

ss 为软引用,指向abc这个对象,abc 会在一定时机被 GC
自动清理,但是ss对象本身的清理工作依赖于queue,当ss出现在queue中时,说明其指向的对象已经无效,可以放心清理ss了。

从上面的分析大家应该对Reference类有了基本的认识,但是上面也提到了,不同的引用,添加到ReferenceQueue的时机是不一样。下面介绍具体引用时再进行说明。
这里有个问题,如果创建引用对象是没有指定ReferenceQueue,引用对象会怎么样呢?这里需要了解Reference澳门新浦京8455com,类内部的四种状态。

使用场景

  至于WeakHashMap的使用场景,目前是在tomcat的ConcurrentCache中使用到了它。其他情况下使用的不多,不过了解了这个对象之后,对我们以后遇到问题的时候,未尝不是一种解决方案呢。

对象可达性判断

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

澳门新浦京8455com 3

java_Reference

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

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

四种状态

每一时刻,Reference对象都处于下面四种状态中。这四种状态用Reference的成员变量queuenext(类似于单链表中的next)来标示。

ReferenceQueue<? super T> queue;
Reference next;

Active。新创建的引用对象都是这个状态,在 GC
检测到引用对象已经到达合适的reachability时,GC
会根据引用对象是否在创建时制定ReferenceQueue参数进行状态转移,如果指定了,那么转移到Pending,如果没指定,转移到Inactive。在这个状态中

//如果构造参数中没指定queue,那么queue为ReferenceQueue.NULL,否则为构造参数中传递过来的queue
queue = ReferenceQueue || ReferenceQueue.NULL
next = null

Pending。pending-Reference列表中的引用都是这个状态,它们等着被内部线程ReferenceHandler处理(会调用ReferenceQueue.enqueue方法)。没有注册的实例不会进入这个状态。在这个状态中

//构造参数参数中传递过来的queue
queue = ReferenceQueue
next = 该queue中的下一个引用,如果是该队列中的最后一个,那么为this

Enqueued。调用ReferenceQueue.enqueued方法后的引用处于这个状态中。没有注册的实例不会进入这个状态。在这个状态中

queue = ReferenceQueue.ENQUEUED
next = 该queue中的下一个引用,如果是该队列中的最后一个,那么为this

Inactive。最终状态,处于这个状态的引用对象,状态不会在改变。在这个状态中

queue = ReferenceQueue.NULL
next = this

有了这些约束,GC
只需要检测next字段就可以知道是否需要对该引用对象采取特殊处理

  • 如果nextnull,那么说明该引用为Active状态
  • 如果next不为null,那么 GC 应该按其正常逻辑处理该引用。

我自己根据Reference.ReferenceHandler.runReferenceQueue.enqueue这两个方法,画出了这四种状态的转移图,供大家参考:

澳门新浦京8455com 4

Reference状态转移图

要理解这个状态 GC 到底做了什么事,需要看 JVM
的代码,我这里时间、能力都不够,就不献丑了,后面有机会再来填坑。
对于一般程序员来说,这四种状态完全可以不用管。最后简单两句话总结上面的四种状态:

  1. 如果构造函数中指定了ReferenceQueue,那么事后程序员可以通过该队列清理引用
  2. 如果构造函数中没有指定了ReferenceQueue,那么 GC 会自动清理引用

总结

  以上呢就是对WeakHashMap的一点浅显的认识了,等有时间了再来深入研究下,简单总结下:

  1. 弱引用对象是由ReferenceHandler守护线程来不断的进行enqueue操作(入队);
  2. 当我们操作WeakHashMap的时候,并不是WeakHashMap自动删除引用队列的引用,而是我们通过间接的调用expungeStaleEntries方法来实现的。

  最后的最后,抛出网上的一道面试题,我觉得这个面试题挺有意思的,既考察了WeakHashMap的使用,又考察了try-catch-finally-return这个点的掌握。

// 求最终打印结果
private static String test(){
    String a = new String("a");
    WeakReference<String> b = new WeakReference<String>(a);
    WeakHashMap<String, Integer> weakMap = new WeakHashMap<String, Integer>();
    weakMap.put(b.get(), 1);
    a = null;
    System.gc();
    String c = "";
    try{
        c = b.get().replace("a", "b");
        return c;
    }catch(Exception e){
        c = "c";
        return c;
    }finally{
        c += "d";
        return c + "e";
    }
}

本文参考了:
Java
内部线程
Java中的WeakHashMap实现分析

面试题地址:
#Java中关于WeakReference和WeakHashMap的理解

ReferenceQueue VS Reference

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

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

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

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

    boolean enqueue(Reference<? extends T> 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;
        }
    }

get

调用Reference.get方法可以得到该引用指向的对象,但是由于指向的对象随时可能被
GC 清理,所以即使在同一个线程中,不同时刻的调用可能返回不一样的值。

Reference与ReferenceQueue之间是如何工作的呢?

    /* List of References waiting to be enqueued.  The collector adds
     * References to this list, while the Reference-handler thread removes
     * them.  This list is protected by the above lock object. The
     * list uses the discovered field to link its elements.
     */
    private static Reference<Object> pending = null;

Reference里有个静态字段pending,当一个Reference的referent被回收时,垃圾回收器会把reference添加到pending这个链表里。

 /**
     * Try handle pending {@link Reference} if there is one.<p>
     * Return {@code true} as a hint that there might be another
     * {@link Reference} pending or {@code false} when there are no more pending
     * {@link Reference}s at the moment and the program can do some other
     * useful work instead of looping.
     *
     * @param waitForNotify if {@code true} and there was no pending
     *                      {@link Reference}, wait until notified from VM
     *                      or interrupted; if {@code false}, return immediately
     *                      when there is no pending {@link Reference}.
     * @return {@code true} if there was a {@link Reference} pending and it
     *         was processed, or we waited for notification and either got it
     *         or thread was interrupted before being notified;
     *         {@code false} otherwise.
     */
    static boolean tryHandlePending(boolean waitForNotify) {
        Reference<Object> r;
        Cleaner c;
        try {
            synchronized (lock) {
                if (pending != null) {
                    r = pending;
                    // 'instanceof' might throw OutOfMemoryError sometimes
                    // so do this before un-linking 'r' from the 'pending' chain...
                    c = r instanceof Cleaner ? (Cleaner) r : null;
                    // unlink 'r' from 'pending' chain
                    pending = r.discovered;
                    r.discovered = null;
                } else {
                    // The waiting on the lock may cause an OutOfMemoryError
                    // because it may try to allocate exception objects.
                    if (waitForNotify) {
                        lock.wait();
                    }
                    // retry if waited
                    return waitForNotify;
                }
            }
        } catch (OutOfMemoryError x) {
            // Give other threads CPU time so they hopefully drop some live references
            // and GC reclaims some space.
            // Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above
            // persistently throws OOME for some time...
            Thread.yield();
            // retry
            return true;
        } catch (InterruptedException x) {
            // retry
            return true;
        }

        // Fast path for cleaners
        if (c != null) {
            c.clean();
            return true;
        }

        ReferenceQueue<? super Object> q = r.queue;
        if (q != ReferenceQueue.NULL) q.enqueue(r);
        return true;
    }

    static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        Thread handler = new ReferenceHandler(tg, "Reference Handler");
        /* If there were a special system-only priority greater than
         * MAX_PRIORITY, it would be used here
         */
        handler.setPriority(Thread.MAX_PRIORITY);
        handler.setDaemon(true);
        handler.start();

        // provide access in SharedSecrets
        SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
            @Override
            public boolean tryHandlePendingReference() {
                return tryHandlePending(false);
            }
        });
    }

同时还通过静态代码块启动了Reference-handler(线程优先级最高),该线程循环执行tryHandlePending方法,参见上方代码。Reference-handler
thread不断的读取pending中的reference,把它加入到对应的ReferenceQueue中。我们可以通过下面代码块来进行把SoftReference,WeakReference,PhantomReference与ReferenceQueue联合使用来验证这个机制。为了确保SoftReference在每次gc后,其引用的referent都被回收,我们需要加入-XX:SoftRefLRUPolicyMSPerMB=0参数,这个原理下文中会在讲。

/**
 * 为了确保System.gc()后,SoftReference引用的referent被回收需要加入下面的参数
 * -XX:SoftRefLRUPolicyMSPerMB=0
 */
public class ReferenceTest {
    private static List<Reference> roots = new ArrayList<>();

    public static void main(String[] args) throws Exception {
        ReferenceQueue rq = new ReferenceQueue();

        new Thread(new Runnable() {
            @Override
            public void run() {
                int i=0;
                while (true) {
                    try {
                        Reference r = rq.remove();
                        System.out.println(“reference:”+r);
                        //为null说明referent被回收
                        System.out.println( “get:”+r.get());
                        i++;
                        System.out.println( “queue remove num:”+i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        for(int i=0;i<100000;i++) {
            byte[] a = new byte[1024*1024];
            // 分别验证SoftReference,WeakReference,PhantomReference
            Reference r = new SoftReference(a, rq);
            //Reference r = new WeakReference(a, rq);
            //Reference r = new PhantomReference(a, rq);
            roots.add(r);
            System.gc();

            System.out.println(“produce”+i);
            TimeUnit.MILLISECONDS.sleep(100);
        }
    }
}

通过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就会入队失败。

软引用(soft reference)

软引用“保存”对象的能力稍逊于强引用,但是高于弱引用,一般用来实现memory-sensitive
caches。

软引用指向的对象会在程序即将触发OOM时被GC
清理掉,之后,引用对象会被放到ReferenceQueue中。

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时会根据两个标准来回收:

  1. SoftReference引用实例的timestamp(每次调用softReference.get()会自动更新该字段
  2. 把最近一次垃圾回收时间赋值给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 <= freeheap *
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
。 github有个基于android源码LruCache改造成的
LruSoftCache,自行验证。

弱引用(weak reference)

软引用“保存”对象的能力稍逊于弱引用,但是高于虚引用,一般用来实现canonicalizing
mapping,也就是本文要讲的WeakHashMap

当弱引用指向的对象只能通过弱引用(没有强引用或弱引用)访问时,GC会清理掉该对象,之后,引用对象会被放到ReferenceQueue中。

WeakReference

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

虚引用(phantom reference)

虚引用是“保存”对象能力最弱的引用,一般用来实现scheduling pre-mortem
cleanup actions in a more flexible way than is possible with the Java
finalization mechanism

调用虚引用的get方法,总会返回null,与软引用和弱引用不同的是,虚引用被enqueued时,GC
并不会自动清理虚引用指向的对象,只有当指向该对象的所有虚引用全部被清理(enqueued后)后或其本身不可达时,该对象才会被清理。

WeakReference应用之WeakHashMap

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

/**
 * 加入下面参数,观察gc情况
 * -verbose:gc
 */
public class WeakHashMapTest {
    private static Map<String,byte[]> 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开始了。

put num: 0 but caches size:1
put num: 1 but caches size:2
[GC (Allocation Failure)  23142K->20936K(125952K), 0.0199681 secs]
put num: 2 but caches size:1
put num: 3 but caches size:2
put num: 4 but caches size:3
[GC (Allocation Failure)  52293K->51672K(159232K), 0.0157178 secs]
put num: 5 but caches size:1
put num: 6 but caches size:2
put num: 7 but caches size:3
put num: 8 but caches size:4
put num: 9 but caches size:5
put num: 10 but caches size:6
[GC (Allocation Failure)  115728K->113064K(191488K), 0.0295324 secs]
[Full GC (Ergonomics)  113064K->61788K(237568K), 0.0172315 secs]
put num: 11 but caches size:1
put num: 12 but caches size:2
put num: 13 but caches size:3
put num: 14 but caches size:4
put num: 15 but caches size:5
put num: 16 but caches size:6
[GC (Allocation Failure)  124511K->123356K(291840K), 0.0174441 secs]
[Full GC (Ergonomics)  123356K->61788K(315392K), 0.0133423 secs]
put num: 17 but caches size:1
put num: 18 but caches size:2
put num: 19 but caches size:3

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<Object> queue = new ReferenceQueue<>();

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

        /**
         * Creates new entry.
         */
        Entry(Object key, V value,
              ReferenceQueue<Object> queue,
              int hash, Entry<K,V> 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<K,V> e = (Entry<K,V>) x;
                int i = indexFor(e.hash, table.length);

                Entry<K,V> prev = table[i];
                Entry<K,V> p = prev;
                while (p != null) {
                    Entry<K,V> 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。
要想WeakHashMap能够释放掉key被GC的value的对象,尽可能的多调用下put/size/get等操作,因为这些方法会调用expungeStaleEntries方法,expungeStaleEntries方法是关键,而如果不操作WeakHashMap,以企图WeakHashMap“自动”释放内存是不可取的,这里的“自动”是指譬如map.put(obj,new
byte[10M]);之后obj=null了,之后再也没掉用过map的任何方法,那么new出来的10M空间是不会释放的。

WeakHashMap.Entry

上面介绍了很多引用的知识点,其实WeakHashMap本身没什么好说的,只要是把引用的作用与使用场景搞清楚了,再来分析基于这些引用的对象就会很简单了。
WeakHashMapHashMap的签名与构造函数一样,这里就不介绍了,这里重点介绍下Entry这个内部对象,因为其保存具体key-value对,所以把它弄清楚了,其他的就问题不大了。

/**
  * The entries in this hash table extend WeakReference, using its main ref
  * field as the key.
  */
 private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
     V value;
     int hash;
     Entry<K,V> next;
     /**
      * Creates new entry.
      */
     Entry(Object key, V value,
           ReferenceQueue<Object> queue,
           int hash, Entry<K,V> next) {
         //这里把key传给了父类WeakReference,说明key为弱引用(没有显式的 this.key = key)
         //所有如果key只有通过弱引用访问时,key会被 GC 清理掉
         //同时该key所代表的Entry会进入queue中,等待被处理
         //还可以看到value为强引用(有显式的 this.value = value ),但这并不影响
         //后面可以看到WeakHashMap.expungeStaleEntries方法是如何清理value的
         super(key, queue);
         this.value = value;
         this.hash  = hash;
         this.next  = next;
     }
     @SuppressWarnings("unchecked")
     //在获取key时需要unmaskNull,因为对于null的key,是用WeakHashMap的内部成员属性来表示的
     public K getKey() {
         return (K) WeakHashMap.unmaskNull(get());
     }
     public V getValue() {
         return value;
     }
     public V setValue(V newValue) {
         V oldValue = value;
         value = newValue;
         return oldValue;
     }
     public boolean equals(Object o) {
         if (!(o instanceof Map.Entry))
             return false;
         Map.Entry<?,?> e = (Map.Entry<?,?>)o;
         K k1 = getKey();
         Object k2 = e.getKey();
         if (k1 == k2 || (k1 != null && k1.equals(k2))) {
             V v1 = getValue();
             Object v2 = e.getValue();
             if (v1 == v2 || (v1 != null && v1.equals(v2)))
                 return true;
         }
         return false;
     }
     public int hashCode() {
         K k = getKey();
         V v = getValue();
         return ((k==null ? 0 : k.hashCode()) ^
                 (v==null ? 0 : v.hashCode()));
     }
     public String toString() {
         return getKey() + "=" + getValue();
     }
 }

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<T> extends PhantomReference<T> {

    private List<Closeable> closeables;

    public ResourcePhantomReference(T referent, ReferenceQueue<? super T> q, List<Closeable> 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<Reference> 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<Closeable> 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<Closeable> 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);

            List<Closeable>closeables=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本身将永远不会可能被加入队列中。

WeakHashMap.expungeStaleEntries

/**
 * Reference queue for cleared WeakEntries
 */
// 所有Entry在构造时都传入该queue
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
/**
 * Expunges stale entries from the table.
 */
private void expungeStaleEntries() {
    for (Object x; (x = queue.poll()) != null; ) {
        synchronized (queue) {
            // e 为要清理的对象
            @SuppressWarnings("unchecked")
                Entry<K,V> e = (Entry<K,V>) x;
            int i = indexFor(e.hash, table.length);
            Entry<K,V> prev = table[i];
            Entry<K,V> p = prev;
            // while 循环遍历冲突链
            while (p != null) {
                Entry<K,V> 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
                    // 可以看到这里把value赋值为null,来帮助 GC 回收强引用的value
                    e.value = null; // Help GC
                    size--;
                    break;
                }
                prev = p;
                p = next;
            }
        }
    }
}

知道了expungeStaleEntries方法的作用,下面看看它是何时被调用的

澳门新浦京8455com 5

expungeStaleEntries调用链

可以看到,在对WeakHashMap进行增删改查时,都调用了expungeStaleEntries方法。

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方法、从undifined队列移除Reference和执行finalize操作。可以看出如果finalize方法中抛出异常会被直接吃掉:

    private void runFinalizer(JavaLangAccess jla) {
        synchronized (this) {
            if (hasBeenFinalized()) return;
            remove();//从undifined队列移除Reference
        }
        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的事情,就是哪些类对象需要入队,何时入队,下面我们一一介绍。

实战

上面说了,下面来个具体的例子帮助大家消化

import java.util.WeakHashMap;
class KeyHolder {
    @Override
    protected void finalize() throws Throwable {
        System.out.println("I am over from key");
        super.finalize();
    }
}
class ValueHolder {
    @Override
    protected void finalize() throws Throwable {
        System.out.println("I am over from value");
        super.finalize();
    }
}
public class RefTest {
    public static void main(String[] args) {
        WeakHashMap<KeyHolder, ValueHolder> weakMap = new WeakHashMap<KeyHolder, ValueHolder>();
        KeyHolder kh = new KeyHolder();    
        ValueHolder vh = new ValueHolder();

        weakMap.put(kh, vh);
        while (true) {
            for (KeyHolder key : weakMap.keySet()) {
                System.out.println(key + " : " + weakMap.get(key));
            }
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("here...");
            //这里把kh设为null,这样一来就只有弱引用指向kh指向的对象
            kh = null;
            System.gc();
        }
    }
}

输出

KeyHolder@a15670a : ValueHolder@20e1ed5b
here...
I am over from key   //输出这句话说明,该key对应的Entry已经被 GC 清理
here...
here...
here...
...
...
...

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

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

总结

说实话,之前我是没怎么了解过引用,更是没有用过WeakHashMap这个类,这次算是把这个坑给填上了。引用的使用场景应该是在常驻类或消耗内存较大应用中才用得上,我自己确实没怎么经历过这种类型的项目,只能现在打好基础,以后有机会在尝试。

其实关于引用,本文重点介绍了弱引用的使用场景,其他的没怎么介绍,感兴趣的可以阅读参考中给出的链接。

何时调用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

instanceOop 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;
}

另外需要提醒的是,当我们通过clone的方式复制一个对象时,如果当前类是一个f类,那么在clone完成时将调用Finalizer.register方法进行注册。

何时入queue

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

Finalizer问题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();
}

finalizee至少两次GC回收才可能被回收?

第一次GC把finalizee对应的Finalizer
reference加入referenceQueue等待FinalizerThread来执行finalize方法。第二次GC才有可能释放finalizee对象本身,前提是FinalizerThread已经执行完finalize方法了,并把Finalizer
reference从Finalizer静态unfinalized链表中剔除,因为这个链表和Finalizer
reference对finalizee构成的是一个强引用

正常情况是最少两次,但是JVM在内存紧张情况下可以立刻回收。

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使用,实现对象回收通知机制。

澳门新浦京8455com 6

image.png

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图