Java引用解析

本文对源码的解析基于JDK13。

1. Java对象的引用

1.1 引用的概念

  谈引用之前,我们先看一下指针,在可以操作指针的语言中,指针代表的是内存的地址,通过操作指针我们可以直接操作内存。Java语言不直接操作指针,即不直接操作内存(但是我们可以通过Unsafe类间接操作内存)。我们所操作的是引用,有了一个对象的引用,我们就可以操作这个对象,而对象分配在内存中,对对象的修改实际上是对内存的读写。所以我们理解的引用本质是别名,实际上引用存储的数值是一块内存的地址。在Java中,如果一块内存存放着另一块内存的地址,我们可以称这块内存为引用。所以Object o = new Object()o本质上是我们刚new出来的对象在内存中地址,无论这个对象是普通对象还是数组对象。

1.2 Reference类解析

  首先,万物皆对象。谈Reference类前,我们先类比Class类,我们知道Class类是对类对象的抽象,Class类的实例表示正在运行的类和接口。Reference类是对引用对象的抽象,与Class类不同的是,Reference类是抽象类,并不能直接实例化。Reference类是与垃圾回收紧密相关的,该类提供了部分接口以便更好的帮助JVM进行GC。它的子类可被实例化,它的子类的实例一般表示了可以在垃圾回收时可以做额外的操作的引用。此外Reference类虽然不用Final修饰,但是不能被直接继承,我们只能从jdk自带的默认实现(SoftReferenceWeakReference等)继承。我们通过对Reference类源码解读加强对引用和垃圾回收的理解。Reference在包java.lang.ref

1.2.1 构造函数

  Reference类没有默认的无参构造函数,只有两个访问权限为包权限的有参构造函数。

	// 构造方法1
	Reference(T referent) {
        this(referent, null);
    }

  上述构造方法本质上是下述构造方法中参数 queue=null的特殊情况。

	// 构造方法2
	Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }

  从构造方法看Reference类至少有两个成员变量:referentquene。其中referent代表该引用对象实际所指向的对象,而queue代表的是引用队列,队列中的元素是Reference对象。当使用后一种构造方法构造Reference对象时,一旦referent所指向对象的GC可达性状态更改后,即对象在进行可达性分析后发现没有与GC Roots相连接的引用链后,简单来说就是没有任何强引用指向referent指向的对象,就会将Reference对象添加进queue所代表的引用队列。此时用户可以使用referent == null或者轮询queue引用队列做相应的清理操作等。

1.2.2 引用队列

  上一节提到了引用队列ReferenceQueue,其节点是ReferenceReference类有一个使用volatile修饰的成员变量next,该属性指向引用队列中的下一个节点:volatile Reference next;。而ReferenceQueue自身持有头节点:private volatile Reference<? extends T> head;
  在ReferenceQueue中有一个enqueue方法,说明了将Reference节点添加进ReferenceQueue引用队列的过程。

	// ReferenceQueue的enqueue方法,仅被Reference类调用
	boolean enqueue(Reference<? extends T> r) {
		// 操作队列时需要加锁
        synchronized (lock) {
            // 构造Reference对象时未传入ReferenceQueue对象,或者Reference对象已经进入队列了
            // 则返回入队失败
            ReferenceQueue<?> queue = r.queue;
            if ((queue == NULL) || (queue == ENQUEUED)) {
                return false;
            }
            // 确认r的队列是否为当前队列
            assert queue == this;
            // 此处为头插法添加节点
            // 如果头节点为null,则将当前节点的下一节点设置为自己,否则将原先头节设置为该节点的下一节点
            r.next = (head == null) ? r : head;
            // 将当前节点设置成头节点
            head = r;
            // 将队列里Reference数量加1
            queueLength++;
            // 将r的queue设置为ENQUEUED状态
            r.queue = ENQUEUED;
            // 如果Reference对象为FinalReference类的实例,则将FinalReference数量也加1
            if (r instanceof FinalReference) {
                VM.addFinalRefCount(1);
            }
            // 唤醒等待的线程
            lock.notifyAll();
            return true;
        }
  	}

  同样的,在ReferenceQueue中有一个reallyPoll方法,说明了将Reference节点移出ReferenceQueue引用队列的过程。

	// reallyPoll该方法是实际将Reference对象移除出队列的过程,poll/remove底层是调用该方法
	// 调用该方法时必须持有锁
	private Reference<? extends T> reallyPoll() {    
		// 获取队列头部的节点   
        Reference<? extends T> r = head;
        if (r != null) {
            r.queue = NULL;
            // 移除头节点,并将头节点的下一个节点设置为头节点
            Reference<? extends T> rn = r.next;
            head = (rn == r) ? null : rn;
            r.next = r;
            queueLength--;
            if (r instanceof FinalReference) {
                VM.addFinalRefCount(-1);
            }
            return r;
        }
        return null;
    }

  结合enqueuereallyPoll方法可以知道ReferenceQuene底层是一个由链表组成的队列,但是该队列实际上后进先出(入队列使用头插法,后来的节点是头节点,出队列移除头节点),实际上更像是一个栈。引用队列大体上的作用是在垃圾回收时可以将Reference放进这个队列中(只有构造Reference对象指定了队列时才入放进去),然后开发人员可以将Reference取出来后执行自己的操作。

1.2.3 对引用的操作

  这里讲的对引用的操作实际讲的就是如何在垃圾回收的时候将Reference对象放进ReferenceQueue队列中。
  Reference类有一个静态代码块,当我们new一个Reference子类的对象出来时,首先会执行该静态代码块。该静态代码块主要工作是新建了一个最高优先级的ReferenceHandler守护线程并启动它。

	static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        Thread handler = new ReferenceHandler(tg, "Reference Handler");
        
        handler.setPriority(Thread.MAX_PRIORITY); // 最高优先级
        handler.setDaemon(true); // 守护线程
        handler.start(); // 启动ReferenceHandler线程

        SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
            @Override
            public boolean waitForReferenceProcessing()
                throws InterruptedException
            {
                return Reference.waitForReferenceProcessing();
            }

            @Override
            public void runFinalization() {
                Finalizer.runFinalization();
            }
        });
    }

  该静态代码块主要工作是新建了一个最高优先级的ReferenceHandler守护线程并启动它。

	private static class ReferenceHandler extends Thread {

        private static void ensureClassInitialized(Class<?> clazz) {
            try {
                Class.forName(clazz.getName(), true, clazz.getClassLoader());
            } catch (ClassNotFoundException e) {
                throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
            }
        }

        static {
        	// 预加载并初始化Cleaner类,防止在后面run循环中懒加载或者初始化Cleaner类内存不足出现异常
            ensureClassInitialized(Cleaner.class);
        }

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

        public void run() {
            while (true) {
                processPendingReferences(); // ReferenceHandler守护线程的工作
            }
        }

  ReferenceHandler类是Reference类的静态内部类,它的run方法是一个无限循环执行processPendingReferences方法将pending-Reference list中的引用对象加入到ReferenceQueue队列中。

	private static void processPendingReferences() {
        // Reference类的native方法,等待JVM在GC时构建pending-Reference list
        waitForReferencePendingList(); 
        Reference<Object> pendingList;
        // 加锁,防止多个线程对pending-Reference list操作产生竞争
        synchronized (processPendingLock) {
        	// Reference类的native方法,原子方式获取pending-Reference list队列并清空该队列
            pendingList = getAndClearReferencePendingList(); 
            processPendingActive = true;
        }
        while (pendingList != null) {
            Reference<Object> ref = pendingList;
            pendingList = ref.discovered; // 获取等待队列中的下一个Reference对象
            ref.discovered = null;
			// Cleaner对象在做清除操作时做额外的处理
            if (ref instanceof Cleaner) {
                ((Cleaner)ref).clean();
                synchronized (processPendingLock) {
                    processPendingLock.notifyAll();
                }
            } else {
                ReferenceQueue<? super Object> q = ref.queue;
                // 在GC回收referent所指向的对象后将Reference对象加入到ReferenceQueue队列中
                if (q != ReferenceQueue.NULL) q.enqueue(ref); 
            }
        }
        synchronized (processPendingLock) {
            processPendingActive = false;
            processPendingLock.notifyAll();
        }
    }

  在这里我们看到了一个新的队列pending-Reference list,以及一个新的成员变量discovered。由pendingList = ref.discovered;可知pending-Reference list通过Reference对象的成员变量discovered连接——private transient Reference<T> discovered;pending-Reference list并不是由用户生成和维护,它是由JVM生成和维护,每次JVM在进行GC的时候,就会生成和填充pending-Reference list队列,队列里是由垃圾收集器维护的需要重新访问的访问的Reference对象,以便确认是否在垃圾回收的时候对用户进行通知——即加入ReferenceQueue队列中。综上所述,当Reference对象所指向的对象被回收时,Reference对象就会进入由JVM维护的pending-Reference list队列,然后由ReferenceHandler线程进行处理,当构建Reference对象传入ReferenceQueue参数后,该Reference对象就会进入到ReferenceQueue队列中,最终起到一个垃圾回收时通知的作用。

  Reference类的API不多,主要有getclearisEnqueuedenqueuereachabilityFence方法。
  get方法返回该Reference对象所实际指向的对象,如果程序或者垃圾收集器清除了该引用对象,则返回null。假设实际指向的对象的强引用已经消失但是还未被垃圾收集器清除,通过该方法返回的对象可以重新变成强引用。
  clear方法清除该Reference对象所实际指向的对象,但是调用该方法并不会导致该引用对象进入引用队列。该方法仅会被java代码调用,垃圾收集器进行垃圾回收时不会调用该方法而可以直接清除引用对象。
  isEnqueued方法告诉用户该Reference对象是否已经进入引用队列(仅当创建Reference对象时传入引用队列时起作用)。
  enqueue方法主动清除该Reference对象所实际指向的对象并将该引用对象加入引用队列(仅当创建Reference对象时传入引用队列时起作用)。该方法仅会被java代码调用,垃圾收集器进行垃圾回收时不会调用该方法而可以直接清除引用对象并将该引用对象如队列。
  reachabilityFence是可达性栅栏,是JDK9新增的方法。调用该方法可以使得一个对象在没有强引用后仍然存活至调用该代码的位置,至少在调用该方法之后,垃圾回收才能回收引用的对象,调用该方法不会导致垃圾回收。常用于Excutors、HTTP2客户端等经常会异步调用的情况。

1.2.4 引用的状态

  官方给引用定义了两种类型的状态,但是引用的状态没有在Reference类中专门定义,而是通过referentqueue等成员变量共同决定。这两种类型的状态Reference对象可同时拥有。
  第一种类型状态有三种,主要和referenct成员变量的值相关,分别是ActivePendingInactive。第二种类型状态有四种,主要和queue成员变量的值相关,分别是RegisteredEnqueuedDequeuedUnregistered
  由Reference类的构造方法可知,创建Reference对象时传入引用队列则引用对象会有RegisteredEnqueuedDequeued三种状态,而未传入引用队列时则只有Unregistered一种状态。引用对象正常存活时则起始状态必然为Active,被垃圾收集器垃圾回收后状态必然为Inactive。则引用对象起始状态有Active/RegisteredActive/Unregistered两种,而终结状态有Inactive/DequeuedInactive/Unregistered两种。故引用状态变迁有Active/Unregistered ->Inactive/UnregisteredActive/Registered->Inactive/Dequeued两种路线。
  第一条:Active/Unregistered ->Inactive/Unregistered
在这里插入图片描述
  第二条:Active/Registered ->Inactive/Registered
在这里插入图片描述
  其中Active/Registered ->Inactive/Registered组合的9种状态中不可能存在Active/EnqeuedActive/Dequeued两种状态,因为enqueue方法会做清除操作,此时不可能是Active状态。

	public boolean enqueue() {
        this.referent = null;  // 此处与clear方法相同
        return this.queue.enqueue(this);
    }

  此外已在上文介绍过GCclearenqueueReferenceHandler等操作,剩余的poll方法和remove方法是ReferenceQueue类的方法,主要做的操作就是清除引用队列的引用对象,所以此时的引用的状态变化均为Enqeued->Dequeued

2. 引用的类型

2.1 强引用

  对于强引用的一般理解就是new出来的引用就是强引用,Object o = new Object()中的o就是强引用,当强引用关系存在时,垃圾回收期就不会回收被强引用引用的对象,当JVM内存不足即将抛出OOM时,GC回收也不会回收强引用所关联的对象内存,会直接抛出OOM。当然上文中提到的reachabilityFence可达性栅栏也能达到强引用关系。在Java类库中没有一个专门的类代表强引用。

2.2 软应用

  软引用用来描述那些有用但是非必需的对象。垃圾收集器会根据内存需求酌情回收仅有软引用所关联的对象:JVM内存不足即将抛出OOM时,触发GC回收软引用所关联的对象内存。而JVM内存充足时,即使进行GC也不会进行回收。在Java类库中SoftReference类代表软引用。在SoftReference类中指出软引用常用于实现对内存敏感的缓存,但其实缓存上的实现我们常用redis

2.3 弱引用

2.3.1 弱引用的概念和作用

  弱引用同样用来描述那些有用但是非必需的对象,通常认为,弱引用的强度要比软引用低,这个强度表现在垃圾回收时:仅有弱引用所关联的对象内存在遭遇GC时就会进行回收,无论当前JVM内存是否足够。在Java类库中WeakReference类代表弱引用。

2.3.2 弱引用的使用场景

  在WeakReference类中指出弱引用常用于实现规范化映射,在JDK中常用于自定义哈希表。具体实现包括ThreadLocal中的ThreadLocalMap实现WeakHashMap实现。
  ThreadLocalMap内部Entry的实现,内部重写了WeakReference类的构造方法public WeakReference(T referent) { super(referent); }ReferenceQueuenull,内部通过get()方法判断引用是否失效:

	static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k); // 调用父类的构造方法,不含ReferenceQueue参数
                value = v;
            }
        }

  WeakHashMap内部Entry的实现,内部重写了WeakReference类的构造方法public WeakReference(T referent, ReferenceQueue<? super T> q) { super(referent, q); }ReferenceQueue为自己声明并传入的队列,具体为queue``private final ReferenceQueue<Object> queue = new ReferenceQueue<>();,内部通过queue.poll()方法判断引用是否失效:

	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); // 调用父类的构造方法,包含ReferenceQueue参数
            this.value = value;
            this.hash  = hash;
            this.next  = next;
        }
    	// 以下省略了Entry内部的equals等方法。
    }

2.4 虚引用

2.4.1 虚引用的概念和作用

  虚引用与软引用、弱引用不同,对一个对象关联虚引用,并不会影响对该对象的GC,仅仅是为了虚引用关联的对象在GC时能收到一个通知,使用者在接收到通知后可以进行事后的清理操作等
  在Java类库中PhantomReference类代表虚引用,与SoftReferenceWeakReference类均有两个构造方法不同,PhantomReference类只有一个构造方法:
public PhantomReference(T referent, ReferenceQueue<? super T> q) { super(referent, q); },即创建虚引用对象时必须传入一个引用队列对象,当referent被回收时,就将虚引用关联的Reference对象加入到引用队列q中,当使用者在引用队列中检查到Reference对象入队,就可以做其他额外的操作,这就是GC时能收到一个通知。

2.4.2 虚引用的使用场景

  虚引用可以用来管理直接内存,即堆外分配的内存。因为垃圾回收器仅可以回收JVM自身所使用的内存,对于直接内存的回收需要使用者自己进行回收,而何时进行回收就需要使用到虚引用的通知作用了。
  PhantomReference有一个子类Cleanernio中的DirectByteBuffer类成员变量cleaner就是在回收直接内存前起到回收通知的作用,后续调用Unsafe类的freeMemory方法回收内存进行清理操作。

2.5 FinalReference

  除了SoftReferenceWeakReferencePhantomReference类外,JDK中还有一个Reference类的子类实现FinalReference。该类是默认的包访问权限,有一个扩展实现Finalizer,该类是包访问权限且使用final修饰,所以该类仅仅为JVM使用。当一个类重写了Object类的finalize方法,该类的对象自创建后就会标记成Finalizer对象以用于JVM进行垃圾回收。

2. 6总结

  对于引用类型,主要记住强软弱虚四种类型,强引用存在不会进行GC回收,软引用存在只在即将发生OOM时进行GC回收,弱引用存在时在下一次GC就会被回收,虚引用不会影响GC回收,只是在回收时起到通知的作用。
参考:
  https://www.jianshu.com/p/30157b64a51a
  https://www.jianshu.com/p/9bb3df3556e2
  https://blog.youkuaiyun.com/gdutxiaoxu/article/details/80738581
  https://www.cnblogs.com/yaowen/p/10841683.html
  https://blog.youkuaiyun.com/zqz_zqz/article/details/79474314
  http://lovestblog.cn/blog/2015/07/09/final-reference/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值