「基础」四种引用类型:强引用、软引用、弱引用、虚引用

java.lang.ref整体包结构:

引用与对象

每种编程语言都有自己操作内存中元素的方式,例如在 C 和 C++ 里是通过指针,而在 Java 中则是通过“引用”。

在 Java 中一切都被视为了对象,但是我们操作的标识符实际上是对象的一个引用(reference)。

//创建一个引用,引用可以独立存在,并不一定需要与一个对象关联
String s;

通过将这个叫“引用”的标识符指向某个对象,之后便可以通过这个引用来实现操作对象了。

String str = new String("abc");
System.out.println(str.toString());

在 JDK1.2 之前,Java中的定义很传统:如果 reference 类型的数据中存储的数值代表的是另外一块内存的起始地址,就称为这块内存代表着一个引用。

Java 中的垃圾回收机制在判断是否回收某个对象的时候,都需要依据“引用”这个概念。

在不同垃圾回收算法中,对引用的判断方式有所不同:

  • 引用计数法:为每个对象添加一个引用计数器,每当有一个引用指向它时,计数器就加1,当引用失效时,计数器就减1,当计数器为0时,则认为该对象可以被回收(目前在Java中已经弃用这种方式了)。
  • 可达性分析算法:从一个被称为 GC Roots 的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。

JDK1.2 之前,一个对象只有“已被引用”和"未被引用"两种状态,这将无法描述某些特殊情况下的对象,比如,当内存充足时需要保留,而内存紧张时才需要被抛弃的一类对象。

四种引用类型

在 JDK.1.2 之后,Java 对引用的概念进行了扩充,将引用分为了:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4 种,这 4 种引用的强度依次减弱。

StrongReference > SoftReference > WeakReference > PhantomReference

强引用

Java中默认声明的就是强引用,比如:

Object obj = new Object(); //只要obj还指向Object对象,Object对象就不会被回收
obj = null;  //手动置null

只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了。

软引用

软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。

在 JDK1.2 之后,用java.lang.ref.SoftReference类来表示软引用。

下面以一个例子来进一步说明强引用和软引用的区别:

在运行下面的Java代码之前,需要先配置参数 -Xms2M -Xmx3M,将 JVM 的初始内存设为2M,最大可用内存为 3M。

首先先来测试一下强引用,在限制了 JVM 内存的前提下,下面的代码运行正常

public class TestOOM {
	
	public static void main(String[] args) {
		 testStrongReference();
	}
	private static void testStrongReference() {
		// 当 new byte为 1M 时,程序运行正常
		byte[] buff = new byte[1024 * 1024 * 1];
	}
}

但是如果我们将

byte[] buff = new byte[1024 * 1024 * 1];

替换为创建一个大小为 2M 的字节数组

byte[] buff = new byte[1024 * 1024 * 2];

则内存不够使用,程序直接报错,强引用并不会被回收

接着来看一下软引用会有什么不一样,在下面的示例中连续创建了 10 个大小为 1M 的字节数组,并赋值给了软引用,然后循环遍历将这些对象打印出来。

public class TestOOM {
	private static List<Object> list = new ArrayList<>();
	public static void main(String[] args) {
	     testSoftReference();
	}
	private static void testSoftReference() {
		for (int i = 0; i < 10; i++) {
			byte[] buff = new byte[1024 * 1024];
			SoftReference<byte[]> sr = new SoftReference<>(buff);
			list.add(sr);
		}
		
		System.gc(); //主动通知垃圾回收
		
		for(int i=0; i < list.size(); i++){
			Object obj = ((SoftReference) list.get(i)).get();
			System.out.println(obj);
		}
		
	}
	
}

打印结果:

我们发现无论循环创建多少个软引用对象,打印结果总是只有最后一个对象被保留,其他的obj全都被置空回收了。

这里就说明了在内存不足的情况下,软引用将会被自动回收。

值得注意的一点 , 即使有 byte[] buff 引用指向对象, 且 buff 是一个strong reference, 但是 SoftReference sr 指向的对象仍然被回收了,这是因为Java的编译器发现了在之后的代码中, buff 已经没有被使用了, 所以自动进行了优化。

如果我们将上面示例稍微修改一下:

private static void testSoftReference() {
	byte[] buff = null;

	for (int i = 0; i < 10; i++) {
		buff = new byte[1024 * 1024];
		SoftReference<byte[]> sr = new SoftReference<>(buff);
		list.add(sr);
	}

	System.gc(); //主动通知垃圾回收
	
	for(int i=0; i < list.size(); i++){
		Object obj = ((SoftReference) list.get(i)).get();
		System.out.println(obj);
	}

	System.out.println("buff: " + buff.toString());
}

则 buff 会因为强引用的存在,而无法被垃圾回收,从而抛出OOM的错误。

如果一个对象唯一剩下的引用是软引用,那么该对象是软可及的(softly reachable)。垃圾收集器并不像其收集弱可及的对象一样尽量地收集软可及的对象,相反,它只在真正 “需要” 内存时才收集软可及的对象。

弱引用

弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收

在 JDK1.2 之后,用 java.lang.ref.WeakReference来表示弱引用。

我们以与软引用同样的方式来测试一下弱引用:

private static void testWeakReference() {
	for (int i = 0; i < 10; i++) {
		byte[] buff = new byte[1024 * 1024];
		WeakReference<byte[]> sr = new WeakReference<>(buff);
		list.add(sr);
	}
	
	System.gc(); //主动通知垃圾回收
	
	for(int i=0; i < list.size(); i++){
		Object obj = ((WeakReference) list.get(i)).get();
		System.out.println(obj);
	}
}

打印结果:

可以发现所有被弱引用关联的对象都被垃圾回收了。

虚引用

虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。

public class PhantomReference<T> extends Reference<T> {
    /**
     * Returns this reference object's referent.  Because the referent of a
     * phantom reference is always inaccessible, this method always returns
     * <code>null</code>.
     *
     * @return  <code>null</code>
     */
    public T get() {
        return null;
    }
    public PhantomReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

那么传入它的构造方法中的 ReferenceQueue 又是如何使用的呢?

引用队列(ReferenceQueue)

引用队列可以与软引用、弱引用以及虚引用一起配合使用,当垃圾回收器准备回收一个对象时,如果发现它还有引用,那么就会在回收对象之前,把这个引用加入到与之关联的引用队列中去。程序可以通过判断引用队列中是否已经加入了引用,来判断被引用的对象是否将要被垃圾回收,这样就可以在对象被回收之前采取一些必要的措施。

与软引用、弱引用不同,虚引用必须和引用队列一起使用。

ReferenceQueue内部数据结构是一个链表,链表里的元素是加入进去的Reference实例,然后通过wait和notifyAll与对象锁实现生产者和消费者,通过这种方式模拟一个队列。

ReferenceQueue是使用wati()和notifyAll()实现生产者和消费者模式的一个具体场景。

ReferenceQueue重点源码解析:

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

这两个静态属性主要用于标识加入引用队列的引用的状态,NULL标识该引用已被当前队列移除过,ENQUEUED标识该引用已加入当前队列。

boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */
    synchronized (lock) {
        //检查该引用是否曾从当前队列移除过或者已经加入当前队列了,如果有则直接返回
        ReferenceQueue<?> queue = r.queue;
        if ((queue == NULL) || (queue == ENQUEUED)) {
            return false;
        }
        assert queue == this;
        r.queue = ENQUEUED;//将引用关联的队列统一标识为ENQUEUED
        r.next = (head == null) ? r : head;//当前引用指向head
        head = r; //将head指向当前引用(链表新增节点采用头插法)
        queueLength++; //更新链表长度
        if (r instanceof FinalReference) {
            sun.misc.VM.addFinalRefCount(1); //
        }
        lock.notifyAll(); //通知消费端
        return true;
    }
}
public Reference<? extends T> remove(long timeout)
    throws IllegalArgumentException, InterruptedException
{
    if (timeout < 0) {
        throw new IllegalArgumentException("Negative timeout value");
    }
    synchronized (lock) {
        Reference<? extends T> r = reallyPoll();
        if (r != null) return r; //如果成功移除则直接返回
        long start = (timeout == 0) ? 0 : System.nanoTime();
        for (;;) {
            lock.wait(timeout); //释放当前线程锁,等待notify通知唤醒
            r = reallyPoll();
            if (r != null) return r;
            if (timeout != 0) {   //如果超时时间不为0则校验超时
                long end = System.nanoTime();
                timeout -= (end - start) / 1000_000;
                if (timeout <= 0) return null;  //如果剩余时间小于0则返回
                start = end;
            }
        }
    }
}

remove尝试移除队列中的头部元素,如果队列为空则一直等待直至达到指定的超时时间。

知识扩展

FinalReference

FinalReference访问权限为package,并且只有一个子类Finalizer,同时Finalizer 是final修饰的类,所以无法继承扩展。

与Finalizer相关联的则是Object中的finalize()方法,在类加载的过程中,如果当前类有覆写finalize()方法,则其对象会被标记为finalizer类,这种类型的对象被回收前会先调用其finalize()。

具体的实现机制是,在gc进行可达性分析的时候,如果当前对象是finalizer类型的对象,并且本身不可达(与GC Roots无相连接的引用),则会被加入到一个ReferenceQueue类型的队列(F-Queue)中。而系统在初始化的过程中,会启动一个FinalizerThread实例的守护线程(线程名Finalizer),该线程会不断消费F-Queue中的对象,并执行其finalize()方法(runFinalizer),并且runFinalizer方法会捕获Throwable级别的异常,也就是说finalize()方法的异常不会导致FinalizerThread运行中断退出。对象在执行finalize()方法后,只是断开了与Finalizer的关联,并不意味着会立即被回收,还是要等待下一次GC,而每个对象的finalize()方法都只会执行一次,不会重复执行

finalize()方法是对象逃脱死亡命运的最后一次机会,如果在该方法中将对象本身(this关键字) 赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移出"即将回收的集合"。

注意:finalize()使用不当会导致内存泄漏和内存溢出,比如SocksSocketImpl之类的服务会在finalize()中加入close()操作用于释放资源,但是如果FinalizerThread一直没有执行的话就会导致资源一直无法释放,从而出现内存泄漏。还有如果某对象的finalize()方法执行时间太长或者陷入死循环,将导致F-Queue一直堆积,从而造成内存溢出(oom)。

Finalizer

1、静态内部类FinalizerThread,是一个守护线程

//消费ReferenceQueue并执行对应元素对象的finalize()方法
private static class FinalizerThread extends Thread {
    ......
    public void run() {
        ......
        final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
        running = true;
        for (;;) {
            try {
                Finalizer f = (Finalizer)queue.remove();
                f.runFinalizer(jla);
            } catch (InterruptedException x) {
            }
        }
    }
}
//初始化的时候启动FinalizerThread(守护线程)
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();
}

2、add方法

在jvm启动的时候就会启动一个守护线程去消费引用队列,并调用引用队列指向对象的finalize()方法。

jvm在注册finalize()方法被覆写的对象的时候会创建一个Finalizer对象,并且将该对象加入一个双向链表中:

static void register(Object finalizee) {
    new Finalizer(finalizee);
}
private Finalizer(Object finalizee) {
    super(finalizee, queue);
    add();
}
private void add() { 
    synchronized (lock) { //头插法构建Finalizer对象的链表
        if (unfinalized != null) {
            this.next = unfinalized;
            unfinalized.prev = this;
        }
        unfinalized = this;
    }
}

另外还有两个附加线程用于消费Finalizer链表以及队列:

Runtime.runFinalization()会调用runFinalization()用于消费Finalizer队列,而java.lang.Shutdown则会在jvm退出的时候(jvm关闭钩子)调用runAllFinalizers()用于消费Finalizer链表。

Cleaner

Cleaner是PhantomReference的一个子类实现,提供了比finalization(收尾机制)更轻量级和健壮的实现,因为Cleaner中的清理逻辑是由Reference.ReferenceHandler 直接调用的,而且由于是虚引用的子类,它完全不会影响指向的对象的生命周期。

一个Cleaner实例记录了一个对象的引用,以及一个包含了清理逻辑的Runnable实例。当Cleaner指向的引用被gc回收后,Reference.ReferenceHandler会不断消费引用队列中的元素,当元素为Cleaner类型的时候就会调用其clean()方法。

Cleaner不是用来替代finalization的,只有在清理逻辑足够轻量和直接的时候才适合使用Cleaner,繁琐耗时的清理逻辑将有可能导致ReferenceHandler线程阻塞从而耽误其它的清理任务。

源码:

public class Cleaner extends PhantomReference<Object>
{
    //一个统一的空队列,用于虚引用构造方法,Cleaner的trunk会被直接调用不需要通过队列
    private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>();

    //Cleaner内部为双向链表,防止虚引用本身比它们引用的对象先被gc回收,此为头节点
    static private Cleaner first = null;

    //添加节点
    private static synchronized Cleaner add(Cleaner cl) {
        if (first != null) {    //头插法加入节点
            cl.next = first;
            first.prev = cl;
        }
        first = cl;
        return cl;
    }
    //移除节点
    private static synchronized boolean remove(Cleaner cl) {

        //指向自己说明已经被移除
        if (cl.next == cl)
            return false;

        //移除头部节点
        if (first == cl) {
            if (cl.next != null)
                first = cl.next;
            else
                first = cl.prev;
        }
        if (cl.next != null)//下一个节点指向前一个节点
            cl.next.prev = cl.prev;
        if (cl.prev != null)//前一个节点指向下一个节点
            cl.prev.next = cl.next;

        //自己指向自己标识已被移除
        cl.next = cl;
        cl.prev = cl;
        return true;

    }

    //清理逻辑runnable实现
    private final Runnable thunk;

    ...

    //调用清理逻辑
    public void clean() {
        if (!remove(this))
            return;
        try {
            thunk.run();
        } catch (final Throwable x) {
            ...
        }
    }
}

Cleaner可以用来实现对堆外内存进行管理,DirectByteBuffer就是通过Cleaner实现堆外内存回收的:

基本原理是创建Cleaner的时候会传入堆外内存对应的引用以及清理内存相关的runnable实现,一旦该引用被回收,则会触发Cleaner相关机制(参见上面讲解)并执行传入的runnable实现中的清理逻辑。

DirectByteBuffer(int cap) { //构造方法中创建引用对象相关联的Cleaner对象                 
    ...
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;
}

private static class Deallocator implements Runnable {
    ...
    public void run() { //内存回收的逻辑(具体实现参看源码此处不展开)
    ...
    }

}     

Reference抽象类

Reference是上面列举的几种引用包括Cleaner的共同父类,一些引用的通用处理逻辑均在这里面实现。

引用实例的几个状态:

  • Active:当处于Active状态,gc会特殊处理引用实例,一旦gc检测到其可达性发生变化,gc就会更改其状态。此时分两种情况,如果该引用实例创建时有注册引用队列,则会进入pending状态,否则会进入inactive状态。新创建的引用实例为Active。
  • Pending:当前为pending-Reference列表中的一个元素,等待被ReferenceHandler线程消费并加入其注册的引用队列。如果该引用实例未注册引用队列,则永远不会处理这个状态。
  • Enqueued:该引用实例创建时有注册引用队列并且当前处于入队列状态,属于该引用队列中的一个元素。当该引用实例从其注册引用队列中移除后其状态变为Inactive。如果该引用实例未注册引用队列,则永远不会处理这个状态。
  • Inactive:当处于Inactive状态,无需任何处理,一旦变成Inactive状态则其状态永远不会再发生改变。

整体迁移流程图如下:

Reference中的几个关键属性:

//关联的对象的引用,根据引用类型不同gc针对性处理
private T referent;       
//引用注册的队列,如果有注册队列则回收引用会加入该队列
volatile ReferenceQueue<? super T> queue;
//上面引用队列referenceQueue中保存引用的链表
/*    active:     NULL //未加入队列前next指向null
 *    pending:    this
 *    Enqueued:   next reference in queue (or this if last)
 *    Inactive:   this
 */
Reference next;


/* When active:   由gc管理的引用发现链表的下一个引用
 *     pending:   pending链表中的下一个元素
 *   otherwise:   NULL
 */
transient private Reference<T> discovered;  /* used by VM */

/* 
 *等待入队列的引用链表,gc往该链表加引用对象,Reference-handler线程消费该链表。
 * 它通过discovered连接它的元素 
 */     
private static Reference<Object> pending = null;

ReferenceHandler:

private static class ReferenceHandler extends Thread {
    ...
    public void run() {
        while (true) {
            tryHandlePending(true); //无限循环调用tryHandlePending
        }
    }
}
static {
    ... jvm启动时以守护线程运行ReferenceHandler
    Thread handler = new ReferenceHandler(tg, "Reference Handler");
    handler.setPriority(Thread.MAX_PRIORITY);
    handler.setDaemon(true);
    handler.start();
    //注册JavaLangRefAccess匿名实现,堆外内存管理会用到(Bits.reserveMemory)
    SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
        @Override
        public boolean tryHandlePendingReference() {
            return tryHandlePending(false);
        }
    });
}
//消费pending队列
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...
                //判断是否为Cleaner实例
                c = r instanceof Cleaner ? (Cleaner) r : null;
               //将r从pending链表移除
                pending = r.discovered;
                r.discovered = null;
            } else {
                // The waiting on the lock may cause an OutOfMemoryError
                // because it may try to allocate exception objects.
                //如果pending没有元素可消费则等待通知
                if (waitForNotify) {
                    lock.wait();
                }
                // retry if waited
                return waitForNotify;
            }
        }
    } catch (OutOfMemoryError x) {
        //释放cpu资源
        Thread.yield();
        // retry
        return true;
    } catch (InterruptedException x) {
        // retry
        return true;
    }

    //调用Cleaner清理逻辑(可参考前面的7,Cleaner段落)
    if (c != null) {
        c.clean();
        return true;
    }
    //如果当前引用实例有注册引用队列则将其加入引用队列
    ReferenceQueue<? super Object> q = r.queue;
    if (q != ReferenceQueue.NULL) q.enqueue(r);
    return true;
}

总结

jvm中引用有好几种类型的实现,gc针对这几种不同类型的引用有着不同的回收机制,同时它们也有着各自的应用场景, 比如SoftReference可以用来做高速缓存, WeakReference也可以用来做一些普通缓存(WeakHashMap), 而PhantomReference则用在一些特殊场景,比如Cleaner就是一个很好的应用场景,它可以用来回收堆外内存。与此同时,SoftReference, WeakReference, PhantomReference这几种弱类型引用还可以与引用队列结合使用,使得可以在关联引用回收之后可以做一些额外处理,甚至于Finalizer(收尾机制)都可以在对象回收过程中改变对象的生命周期。

 

posted @ 2022-01-23 09:23  残城碎梦  阅读(6898)  评论(0编辑  收藏  举报