引用
在我们的开发工作中,引用无处不在,可以说我们的工作就是通过引用来对对象进行各种操作。在JDK的<java.lang.ref>包下对引用进行了定义Reference,在源码中对引用定义了四个状态,分别是活跃(Active)、挂起(Pending)、入队(Enqueued)、死亡(Inactive)。正常来说,每个引用的生命周期都会经历这四个状态。
生命周期
构造函数
-
T referent:引用的对象,被GC特殊处理的对象。
-
ReferenceQueue:即将销毁的引用队列,在创建引用时提供。当引用即将被销毁时,会将引用压入这个队列。构造引用时是否提供这个参数将决定引用的生命周期是否会经历等待处理(Pending)和入队(Enqueued)。
状态
Reference内部并没有一个指定的字段来记录引用状态,而是通过引用的next指针、引用队列以及discovered来区分。一个引用实例可能处于四种内部状态:
-
活跃(Active)
被垃圾收集器特殊对待的状态。当垃圾收集器检测到可触达的引用变为适当的状态后,引用的状态会变为Pending或者Inactive,这取决于引用在创建时是否提供了队列。如果在创建时提供了队列,那么这个引用会被添加到pending-Reference列表中(注意:这个队列并不是创建时提供的队列);否则将直接变为Inactive状态。
-
挂起(Pending)
这个状态的引用都处于pending-Reference列表中,等待引用处理线程(Reference-handler thread)将其处理(这个过程中如果引用是个Cleaner,将会调用其自定义的clean()方法,这个是重点,使用案例参照DirectByteBuffer)并加入引用队列,改为enqueued状态。
-
入队(Enqueued)
在创建引用时可以指定一个引用队列,当引用即将销毁的时候会将引用压入这个队列中。
-
死亡(Inactive)
引用的最终状态。当引用从引用队列出弹出时就进入了Inactive状态,此后将不再做任何操作。
引用的不同状态下,next、discovered以及queue的区别,可以看出无论是哪一种状态,总能通过这next和queue来确定引用的唯一状态。
states next queue discovered active null 创建时指定的queue或者ReferenceQueue.NULL next element in a discovered reference list maintained by GC (or this if last) pending this 创建时指定的queue next element in the pending list (or null if last) Enqueued next reference in queue (or this if last) ReferenceQueue.ENQUEUED null Inactive this ReferenceQueue.NULL null 说明:
1.垃圾收集器需要检查next来决定是否一个引用实例需要特殊处理:如果next是null那么这个实例是active;如果不是null,那么垃圾收集器将正常处理这个应用实例。 2.为了确保一个并发的垃圾收集器可以在不干扰应用线程enqueue()对象的情况下发现活跃的引用对象,垃圾收集器 应该通过discovered字段将被发现的对象做成链表。discovered字段同时也用于在pending集合中链住引用对象。
引用类型
在JDK 1.2以前的版本中,若一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于可触及(reachable)状态,程序才能使用它。从JDK 1.2版本开始,把对象的引用分为4种级别,从而使程序能更加灵活地控制对象的生命周期。这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用。
强引用(StrongReference)
强引用是使用最普遍的引用。在代码中表现为等于号,当我们将一个变量等于一个对象时,这个变量便对这个对象添加了强引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
Object o = new Object();
软引用(SoftReference)
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
SoftReference<byte[]> objectSoftReference = new SoftReference<>(new byte[1024*1024*10]);
ReferenceQueue<byte[]> referenceQueue = new ReferenceQueue<>();
SoftReference<byte[]> objectSoftReference = new SoftReference<>(new byte[1024*1024*10],referenceQueue);
弱引用(WeakReference)
弱引用。当一个对象被弱引用封装且不存在其他任何引用时,一旦垃圾收集器扫描到这个对象,不管内存是否足够,将直接回收这个对象的内存。假设垃圾收集器在某个时间点确定一个对象是弱可及的,那么他将会清除这个对象的所有弱引用,以及通过这个对象的强引用链和软引用链所能触达的其他任何弱可及对象的所有弱引用。同时,它将声明所有这些弱可达的对象都是可回收的,随后将会把这些刚清除的弱引用压入各自的引用队列中。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
弱引用与软引用的区别在于,当被垃圾收集器扫描到后,软引用会在内存不足时进行释放,弱引用会直接释放,只具有弱引用的对象拥有更短暂的生命周期。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
WeakReference<byte[]> weakReference = new WeakReference<>(new byte[1024 * 1024 * 10]);
ReferenceQueue<byte[]> referenceQueue = new ReferenceQueue<>();
WeakReference<byte[]> weakReference = new WeakReference<>(new byte[1024 * 1024 * 10],referenceQueue);
虚引用(PhantomReference)
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
ReferenceQueue queue = new ReferenceQueue ();
PhantomReference pr = new PhantomReference (object, queue);
-
虚引用并不会决定对象的生命周期
-
虚引用必须和引用队列关联使用, 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之关联的引用队列中
-
程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动
引用类型 | 构建方式 | 特点 | 使用场景 |
---|---|---|---|
强应用 | 普通引用,=指向一个对象 | 当一个对象上有强应用的时候,永远不会被GC回收。 | 使用期间不会被强制gc回收的场景 |
软引用 | new SoftReference<>(T referent) | 当堆内存不够时,会强制回收。 | 内存敏感的缓存,当内存不足时,不能占用内存。 |
弱引用 | new WeakReference<>(T referent) | 被gc扫描到就会回收 | ThreadLocalMap防止内存泄漏,监控对象是否将要被回收 |
虚引用 | new PhantomReference (object, queue) | 虚引用并不会决定对象的生命周期。但是可以在一个对象被回收前收到通知。 | DirectByteBuffer、gc监控 |
使用案例
弱引用
ThreadLocal 线程本地变量
ThreadLocal被定义在jdk的<java.lang>包下面,他的作用在于辅助开发者操作线程本地变量,让我们可以在繁琐复杂的方法调用链中灵活的获取线程本地变量,而不用通过鸡肋的引用传递来获取N层调用次数之前的栈内变量。那为什么说他是辅助呢?因为真正的线程本地变量是通过ThreadLocalMap来储存的,而ThreadLocal封装了从当前线程中获取ThreadLocalMap的方法,我们可以通过get()、set(T)方法方便的对当前线程的栈内变量进行操作。这里有一点需要注意一下,ThreadLocal只具备操作当前线程的本地变量能力,并不能从当前线程直接获取其他线程的本地变量,而这一特性也经常被用来实现线程隔离,例如请求上下文、session获取等等。下面我们就来详细的分析一下ThreadLocal的原理及用法。
原理
在认识ThreadLocal之前,我们需要掌握一个前提,那就是Thread是怎么实现本地变量存储的。首先,jdk在Thread中定义了一个变量ThreadLocalMap用于存储线程本地变量,而这个ThreadLocalMap是一个自定义的HashMap结构,他的元素Entry同时也继承了WeakReference是一个弱引用,这个弱引用引用的对象就是Entry的Key即ThreadLocal对象,而Entry的值value是个强引用的对象。当我们需要将对象存入线程本地时,调用ThreadLocal#set(T)方法,ThreadLocal会取出当前线程的本地变量表ThreadLocalMap,将当前ThreadLocal和T以K-V形式存入这个Map中,至此便完成了存储过程;当我们在想取出时,只需要调用ThreadLocal#get()方法,取出线程的本地变量表ThreadLocalMap,以相同的算法计算hashkey,取出Value即可。
ThreadLocal的引用关系:
ThreadLocal.set(T)流程
ThreadLocal.get()流程
为什么使用弱引用?
这么设计的目的在于,当ThreadLocal这个对象没有任何强引用和软引用指向它的时候,代表我们在程序中将不会以任何一种方式可触达的方式主动获取到这个ThreadLocal,也就是说我们再也用不到这个Key所指向的ThreadLocalMap.Entry了,那么垃圾收集器在扫描到它的时候会尽快回收这个ThreadLocal的内存地址,从而避免造成内存泄漏。
弱引用的结构设计有什么不足?
弱引用的设计解决了ThreadLocalMap.Entry的key,即ThreadLocal的内存泄漏,但是,这样做真的就万无一失了吗?细心的同学应该已经发现了问题,Key的内存泄露就解决了,那么Value呢?Value在Entry是个强引用类型,既然线程还没有终止,那么ThreadLocalMap的引用就是有效的,他的Entry就不会被清理,这样内存中保存在Value的引用,但是我们再也不会获取到这块内存,这样不就造成了Value的内存泄漏吗?没错!ThreadLocal和Value都只是Entry中的引用,ThreadLocal的弱引用关系被清理后可以回收ThreadLocal的内存,但是由于Entry仍然被Map所引用,导致Value无法被回收,进而造成Value的内存泄露。
Value内存泄漏发生的条件
-
持有ThreadLocalMap的线程处于存活状态
-
ThreadLocal的所有引用被显示取消,包括当前线程、其他线程、静态变量等。
常见的场景有:
- 在使用了线程池中的某个核心线程提交了任务以后,在任务中存储了本地变量并使用,当任务结束后并没有显示的调用remove,而核心线程是长期存活的,不会被清理,从而导致内存泄漏。
- 在代码中使用了静态变量ThreadLocal用于在整个项目中使用,在执行某个任务时不小心将ThreadLocal置空,从而导致存活的线程Value获取不到且无法被释放。
如何避免Value内存泄漏?
在使用完线程本地变量后,一定要显示的调用ThreadLocal#remove()方法。在remove方法中,ThreadLocal会获取到ThreadLocalMap调用其remove方法,从而清除Entry、key、value的所有引用关系,GC收集器扫描到后会进行内存回收,避免泄漏。
虚引用
DirectByteBuffer
DirectByteBuffer通常被称为堆外内存或者直接内存,通过DirectByteBuffer申请的内存空间不会被GC管理回收,为了不造成内存泄漏,JDK在设计DirectByteBuffer时为其添加了自动回收机制。利用虚引用的特性,跟踪引用状态,当引用失效时调用清理方法进行内存回收。这里是虚引用的一个典型使用案例,接下来我们看看具体是怎么设计的。
原理
-
首先在调用构造函数申请DirectByteBuffer时,通过Unsafe分配直接内存映射,这里使用代理模式,DirectByteBuffer的所有IO操作都通过Unsafe来完成。
-
在申请完内存后会同时创建一个Cleaner,Cleaner是一个指向DirectByteBuffer的虚引用,同时在Cleaner内部构造了一个静态的引用队列,用于存放整个项目内所有即将死亡的Cleaner引用。Cleaner内部维护了一个所有已提交的Cleaner链表和一个清理函数,这个清理函数会在引用处理线程将引用从活跃状态改为挂起状态时进行调用。而DirectByteBuffer在其内部设计了基于Unsafe内存释放的清理函数进行提交,构建Cleaner。
-
当我们在使用完DirectByteBuffer后,线程终止。DirectByteBuffer引用失效,届时,GC垃圾收集器扫描到DirectByteBuffer后,会顺着引用链找到其内部的Cleaner,此时Cleaner除了DirectByteBuffer的引用外只有虚引用,GC会将其标记为可回收状态,由于Cleaner同时也是虚引用,GC会将Cleaner压入全局引用挂起链表中。
-
当Cleaner被压入挂起链表后,由于ReferenceHandler线程在不断的扫描挂起链表中的每一个引用,将其从挂起链表中剔除。同时,如果这个引用是Cleaner类型,则会回调Cleaner#clean()函数,这个函数便是DirectByteBuffer中定义的内存释放函数,届时,Unsafe会回收直接内存,从而避免内存泄漏。
-
Cleaner从挂起链表移除后,会进入引用队列中改为enqueued状态,当期从引用队列弹出后,便会改为最终状态Inactive。
每一个DirectByteBuffer的申请都伴随着一个Cleaner的创建,当DirectByteBuffer不在引用时,依靠引用的特性回调清理函数,达到自动释放内存的目的。