本篇文章主要是总结下java中的各种引用即:强引用、软引用、弱引用、虚引用。
大多情况下,Java中对象并不是从GC Roots集直接引用的,目前hotspot主要使用两类作为gc roots:
一般来说,对象都是被若干其他对象引用,从而构成一个以根集为顶的树形结构
在这个树形的引用链中,箭头的方向代表了引用的方向,所指向的对象是被引用对象。由图可以看出,从GC Roots集到一个对象可以有很多条路。比如到达对象5的路径就有①-⑤,③-⑦。可达性路径判定规则:
比如,我们假设图中引用①和③为强引用,⑤为软引用,⑦为弱引用,对于对象5按照这两个判断原则,路径①-⑤取最弱的引用⑤,因此该路径对对象5的引用为软引用。同样,③-⑦为弱引用。在这两条路径之间取最强的引用,于是对象5是一个软可达对象。
引用是JAVA中默认采用的一种方式,我们平时创建的引用都属于强引用。
如果一个对象没有强引用,那么对象就可能会被回收。
使用强引用一定要注意避免内存泄露。
测试代码如下:
public void strongReferenceTest(){
Object obj = new Object();
Object objRef = obj;
obj = null;
System.gc();
System.out.println(obj);
}
如果一个对象只具有软引用那这个对象有以下特点:
示例如下:
public void softReferenceTest(){
String str=new String("abc"); // 强引用
SoftReference<String> softRef=new SoftReference<String>(str); // 软引用
str = null;
// 当内存不足时,等价于:
/* If(JVM.内存不足()) {
str = null; // 转换为软引用
System.gc(); // 垃圾回收器进行回收
}*/
}
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。
当你想引用一个对象,但是这个对象有自己的生命周期,你不想介入这个对象的生命周期,此时最佳方式就是弱引用。
在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联 的引用队列中。
示例代码如下:
public void weakReferenceTest(){
// str强引用
String str = new String("hello");
// 引用队列
ReferenceQueue<String> rq = new ReferenceQueue<String>();
// 弱引用,关联了str rq队列
WeakReference<String> wf = new WeakReference<String>(str, rq);
// 取消"hello"对象的强引用
str=null;
// 强制gc
// 注意,实际场景中要慎用 System.gc(); 会导致FUll gc即整个heap gc
System.gc();
// 假如"hello"对象没有被回收,str1会强引用"hello"对象
String str1=wf.get();
System.out.println("str1 = " + str1);
// 假如"hello"对象没有被回收,rq.poll()返回null
Reference<? extends String> ref=rq.poll();
System.out.println("ref = " + ref);
}
注意,加入引用队列的引用对象不会被自动清理,需要手动调用poll,然后将拿到的对象设为null
帮助GC快速清理这个已经无用的引用实例。
WeakHashMap
是一个特殊的map,有着弱引用的key
。也就是说,当某个key没有再被正常使用时(被GC干掉了),WeakHashMap中的这个key
对应的entry
会被自动高效移除。这一点是WeakHashMap与其他map最大的不同之处。
但要注意的是,WeakHashMap的自动移除entry前提是对WeakHashMap进行了访问。大多数访问行为会自动调用其expungeStaleEntries
方法,清理无效key对应的entry。也就是说,不访问WeakHashMap的话即使key已经变为null,对应的entry也不会被回收!
当使用 WeakHashMap 时,即使没有显示的添加或删除任何元素,也可能发生如下情况:
WeakHashMap是软引用的一个典型的应用,更多信息请点击Java学习-容器-WeakHashMap
虚引用形同虚设,它所引用的对象随时可能被垃圾回收。
虚引用与软引用和弱引用的不同之处在于必须和引用队列一起使用:
private static final ReferenceQueue<Object> queue = new ReferenceQueue<>();
new PhantomReference<>(object, queue);
虚引用的使用场景很窄,在JDK中,目前只知道在申请堆外内存时有它的身影。申请堆外内存时,在JVM堆中会创建一个对应的Cleaner对象,这个Cleaner类继承了PhantomReference,当DirectByteBuffer对象被回收时,可以执行对应的Cleaner对象的clean方法,做一些后续工作,这里是释放之前申请的堆外内存。
由于虚引用的get方法无法拿到真实对象,所以当你不想让真实对象被访问时,可以选择使用虚引用。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
看到有些文章说虚引用可以清理已经执行finalize方法,但是还没被回收的对象,这简直就是误导人嘛,与finalize方法有关的引用是FinalReference,这个引用就是之前说的其它两种中的一个。
首先要说明一点,虽然ReferenceQueue
名为queue
,但其实不是一个queue,而更像一个用链表实现的stack
,因为入队元素是后进先出的。
前面多次提到的ReferenceQueue
,GC在检测到适当的可达性状态(根据引用类型不同而不同)发生变化后会将注册的引用放入引用队列,方便我们处理它,因为引用对象指向的对象会被GC自动清理。但是引用对象本身也是对象(是对象就占用一定资源),所以需要我们自己清理。看这个小示例:
// str强引用
String str = new String("hello");
// 引用队列
ReferenceQueue<String> rq = new ReferenceQueue<String>();
// wf是一个WeakReference类型的强引用,有一个弱引用指向str
WeakReference<String> wf = new WeakReference<String>(str, rq);
// str强引用消除,wf指向str的弱引用会在gc后被干掉。但是wf这个对象还在,需要手动处理
str=null
public class ReferenceQueue<T> {
/**
* Constructs a new reference-object queue.
*/
public ReferenceQueue() { }
可以看到ReferenceQueue
类有一个泛型T
// 静态内部类Null 继承自ReferenceQueue,泛型和ReferenceQueue的相同
private static class Null<S> extends ReferenceQueue<S> {
boolean enqueue(Reference<? extends S> r) {
return false;
}
}
static ReferenceQueue<Object> NULL = new Null<>();
static ReferenceQueue<Object> ENQUEUED = new Null<>();
// 静态内部类Lock
static private class Lock { };
// Lock实例
private Lock lock = new Lock();
// 队列的头,初始值为null
private volatile Reference<? extends T> head = null;
// 队列长度
private long queueLength = 0;
// 这个方法只会被Reference对象调用
boolean enqueue(Reference<? extends T> r) {
synchronized (lock) {
// 这里的queue就是在Reference创建时与之关联的ReferenceQueue
ReferenceQueue<?> queue = r.queue;
// 检查在拿到锁之后,该引用还没有入队(甚至是被移除掉了)
if ((queue == NULL) || (queue == ENQUEUED)) {
return false;
}
assert queue == this;
// 表示该引用的关联的引用队列状态为已入队?
r.queue = ENQUEUED;
// 该引用next指向原来队列的头部,然后head指针移动到当前引用处
r.next = (head == null) ? r : head;
head = r;
// 引用队列长度自增
queueLength++;
// 如果引用是FinalReference就干啥。。这里没看懂
if (r instanceof FinalReference) {
sun.misc.VM.addFinalRefCount(1);
}
//将wait在此锁上的线程全部唤醒
lock.notifyAll();
return true;
}
}
值得注意的是,这个ReferenceQueue名字为队列,其实是个栈,因为入队时是用的头插法,也就是说会先进后出。
/* Must hold lock */
// 真正获取并删除队列中头结点
private Reference<? extends T> reallyPoll() {
// 获取引用队列头结点Reference
Reference<? extends T> r = head;
if (r != null) {
// r.next == r表示只有一个引用结点,此时head置为null
// 否则将head指针移动到r.next处
head = (r.next == r) ?
null :
r.next; // Unchecked due to the next field having a raw type in Reference
// poll后需要将r.queue置为空队列
r.queue = NULL;
// 这里的操作就是将r.next指向本身,相当于r和head之间就已经没有联系了
r.next = r;
// 队列长度减一
queueLength--;
if (r instanceof FinalReference) {
sun.misc.VM.addFinalRefCount(-1);
}
// 返回该reference
return r;
}
return null;
}
public Reference<? extends T> poll() {
if (head == null)
return null;
synchronized (lock) {
return reallyPoll();
}
}
如果引用队列头为空,就立即返回null
;否则就尝试获取锁,然后去执行reallyPoll
来获取队列头引用
remove
方法有两个,先来看带有timeout
参数的方法:
public Reference<? extends T> remove(long timeout)
throws IllegalArgumentException, InterruptedException
{
if (timeout < 0) {
throw new IllegalArgumentException("Negative timeout value");
}
synchronized (lock) {
// 拉队列头的reference
Reference<? extends T> r = reallyPoll();
// 不为空就直接返回
if (r != null) return r;
// 走到这里说明没有拉取到reference
long start = (timeout == 0) ? 0 : System.nanoTime();
// 开始根据timeout循环等待
for (;;) {
// 这个地方wait唤醒有两种情况
// 一种是timeout时间到,那么会结束循环
// 另一种是被enqueue方法加入引用成功后唤醒,此时会尝试拉取队头
lock.wait(timeout);
r = reallyPoll();
if (r != null) return r;
// 如果设的超时时间不为0,就把总超时时间减去消耗的时间。
// 如果timeout为0 会无限循环这个过程
if (timeout != 0) {
long end = System.nanoTime();
timeout -= (end - start) / 1000_000;
// 剩余超时时间小于0,就返回null
if (timeout <= 0) return null;
// 否则重置start为当前时间,然后进入下一次循环
start = end;
}
}
}
}
该方法用来移除引用队列中的下一个reference。当前线程会阻塞直到返回一个reference对象或给定的超时时间到了。
另一个不带参数的方法其实就是调用我们上面提到的方法,传入参数0
,也就是说会永久等待直到从引用队列中拿到可用的reference:
public Reference<? extends T> remove() throws InterruptedException {
return remove(0);
}
public abstract class Reference<T>
Reference是所有引用对象的基抽象类,他定义了一些通用的操作。因为引用对象的实现和GC紧密关联,所以该类可能不会直接进行子类化。
新创建的Reference实例状态就为Active
当GC发现被该Reference引用的对象的可达性已经变为恰当的状态时,会根据该Reference实例在创建时是否已经注册到一个引用队列,来判断将该Reference实例的状态变更为Pending
或Inactive
。 在前一种(注册到RQ
)总是会将该Reference实例添加到pending-Reference
列表。
Pending
状态表示pending-Reference
列表中的元素等待被Reference-handler
线程入队。
没有注册到RQ
的Reference实例不会进入Pending
状态。
Pending
状态表示该Reference实例是创建时就注册到的RQ中的元素,而且该引用指向的对象已经为待回收状态,并且该Reference实例已经放到RQ当中了。当一个Reference实例被从RQ中移除时,状态会迁移到Inactive
。
没有注册到RQ
的Reference实例不会进入Enqueued
状态。
即此Reference对象已经由外部从queue中获取到,并且已经处理掉了。也就是说,此Reference对象可以被回收,并且其内部封装的对象也可以被回收( 实际的回收运行取决于clear动作是否被调用 )。可以理解为进入到Inactive
状态的肯定是应该被回收掉的。
Reference实例处于Inactive
这个状态以后不会再做任何操作,也不会再变化。
JVM并不需要定义上述的状态值来判断相应引用的状态处于哪个状态,只需要通过以下计算next
和queue
即可:
状态 | queue | next |
---|---|---|
Active | 如果RQ对象为空或者没有传入,则为ReferenceQueue.NULL;否则queue为创建一个Reference对象时传入的RQ对象; | null |
Pending | 初始化时引用注册的RQ对象 | this |
Enqueue | ReferenceQueue.ENQUEUED | 为RQ中下一个要处理的Reference对象;如果本对象已经是队列尾部,那就是this; |
Inactive | ReferenceQueue.NULL | this |
通过这个组合,GC如果需要抉择一个Reference实例是否需要特殊处理时,只需要检测next
:
Active
状态;为了确保并发垃圾收集器
能够发现Active
状态的Reference对象,而且不干扰可能将enqueue()方法应用于这些对象的应用程序线程,收集器应通过discovered
字段链接(link) discovered
的对象。discovered
字段也用于链接pending
列表中的Reference对象。
// 引用对象
private T referent; /* Treated specially by GC */
// 引用队列,但其实只是用来标识是空队列还是已经创建的队列
volatile ReferenceQueue<? super T> queue;
/**
* 指向ReferenceQueue中的下一个Reference节点
* When active: NULL,此时还没有加入ReferenceQueue
* pending: this,此时指向自身
* Enqueued: 队列中的下一个引用对象,或已经是最后一个时就指向自己
* Inactive: this,此时指向自身
*/
Reference next;
/* When active: 由GC维护的一个已发现的reference列表中的下一个元素(或已经是最后一个时就指向自己)
* pending: pending列表中的下一个元素(或已经是最后一个时就指向自己)
* otherwise: NULL
*/
transient private Reference<T> discovered; /* used by VM */
/**
*
* 被GC使用的同步锁,当GC在每次垃圾搜集前必须先获取此锁。
* 所以这个锁至关重要,每个持有该锁的代码必须尽快完成从而释放锁,不要使用新对象、调用其他用户代码
*/
static private class Lock { }
private static Lock lock = new Lock();
/**
* 等待入队的reference对象列表
* 由GC将引用添加到此队列,同时Reference-handler线程负责移除他们
* 这个列表由前面提到的Lock对象进行同步锁
*
* 这个list还会使用已发现的域来连接里面的元素
*/
private static Reference<Object> pending = null;
// 高优先级的线程来将pending状态的reference入队
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 {
// 预加载和初始化以下两个类,避免在后面代码中出现加载顺序问题
ensureClassInitialized(InterruptedException.class);
ensureClassInitialized(Cleaner.class);
}
ReferenceHandler(ThreadGroup g, String name) {
super(g, name);
}
public void run() {
while (true) {
// 处理pending状态的reference
tryHandlePending(true);
}
}
}
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
// 为当前线程组和其父线程组构建并启动pending状态reference的处理线程
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
// 构建一个处理pending状态reference的处理线程
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 提供一个java.lang的访问特权
SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
@Override
public boolean tryHandlePendingReference() {
return tryHandlePending(false);
}
});
}
// 不带引用队列的版本
Reference(T referent) {
this(referent, null);
}
// 带引用队列的构造方法
Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
// 若干引用队列为空就用ReferenceQueue.NULL表示空队列,不允许加入元素
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
/**
* 尝试处理pending状态的reference
*
* 当返回true的时候代表还有其他pending状态的reference
* 返回false代表没有其他reference节点了,线程可以干其他有意义的事情而不是继续循环
*
* @param waitForNotify
* 该参数为true且没有pending状态的reference时,就开始等待直到VM notify或是被中断
* 为false代表没有pending状态的reference时立刻返回
*
* @return
* true: 表示存在一个已经被处理的pending状态的reference,
* 或是我们等待通知并且在收到通知之前得到它或者线程被中断;
* false: 与true相反
*/
static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
// 这个GC器继承自PhantomReference
Cleaner c;
try {
// 同步锁
synchronized (lock) {
// 注意,我们之前提到过pending状态的reference是由GC线程负责添加
if (pending != null) {
r = pending;
// 'instanceof'操作可能导致OOM,所以这个操作要在移除r到pending链之前
c = r instanceof Cleaner ? (Cleaner) r : null;
// 移除r和pending链之间的连接
// 把r发现的加入pending链等待入队
pending = r.discovered;
r.discovered = null;
} else {
// 此时pending为空,即没有等待入队的reference
// 等待锁过程可能导致OOM
// 因为可能尝试分配exception对象
if (waitForNotify) {
lock.wait();
}
// retry if waited
return waitForNotify;
}
}
} catch (OutOfMemoryError x) {
// 此时执行Thread.yield(),尝试放弃CPU运行权
// 可以给其他线程CPU时间,给其他线程CPU时间,希望他们删除一些存活的引用,让GC回收一些空间
// 如果上面的'r instanceof Cleaner'持续抛出OOM一段时间,也可以防止CPU集中式地自旋
Thread.yield();
// retry
return true;
} catch (InterruptedException x) {
// retry
return true;
}
// 此时pending != null
// c是Cleaner,清理并返回
if (c != null) {
c.clean();
return true;
}
// 否则pengind不为空,且不是Cleaner
// 此时就将该pending元素入队到ReferenceQueue
ReferenceQueue<? super Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}
/**
* 返回当前reference所引用的对象
* 如果该引用对象已经程序或是GC被清除,那么该方法会返回null
*
*/
public T get() {
return this.referent;
}
/**
* 清理所引用的对象
* 调用此方法不会导致引用的对象入队到ReferenceQueue
*
* 注意,此方法只会被java用户代码调用,因为GC不需要调用此方法就能清理对象
*/
public void clear() {
this.referent = null;
}
Reference
就是一个强引用对象,同时他有一个引用指向目标对象。GC会在清理指向的引用对象时将该Reference放入ReferenceQueue,用户还需要自行清理这个Reference
对象。
在以下例程的References类中,依次创建了10个软引用、10个弱引用和10个虚引用,它们各自引用一个Grocery对象。
import java.lang.ref.*;
import java.util.*;
class Grocery {
private static final int SIZE = 10000;
// 属性d使得每个Grocery对象占用较多内存,有80K左右
private double[] d = new double[SIZE];
private String id;
public Grocery(String id) {
this.id = id;
}
public String toString() {
return id;
}
// 通过finalize方法观察该对象被gc认为可以回收
public void finalize() {
System.out.println("Finalizing " + id);
}
}
public class References {
private static ReferenceQueue<Grocery> rq = new ReferenceQueue<Grocery>();
public static void checkQueue() {
// 从队列中取出一个引用
Reference<? extends Grocery> inq = rq.poll();
if (inq != null)
System.out.println("In queue: " + inq + " : " + inq.get());
}
public static void main(String[] args) {
final int size = 10;
// 创建10个Grocery对象以及10个软引用
Set<SoftReference<Grocery>> sa = new HashSet<SoftReference<Grocery>>();
for (int i = 0; i < size; i++) {
SoftReference<Grocery> ref = new SoftReference<Grocery>(
new Grocery("Soft " + i), rq);
System.out.println("Just created: " + ref.get());
sa.add(ref);
}
System.gc();
checkQueue();
// 创建10个Grocery对象以及10个弱引用
Set<WeakReference<Grocery>> wa = new HashSet<WeakReference<Grocery>>();
for (int i = 0; i < size; i++) {
WeakReference<Grocery> ref = new WeakReference<Grocery>(
new Grocery("Weak " + i), rq);
System.out.println("Just created: " + ref.get());
wa.add(ref);
}
System.gc();
checkQueue();
// 创建10个Grocery对象以及10个虚引用
Set<PhantomReference<Grocery>> pa = new HashSet<PhantomReference<Grocery>>();
for (int i = 0; i < size; i++) {
PhantomReference<Grocery> ref = new PhantomReference<Grocery>(
new Grocery("Phantom " + i), rq);
System.out.println("Just created: " + ref.get());
pa.add(ref);
}
System.gc();
checkQueue();
}
}
从程序运行时的打印结果可以得出以下结论:
Java8-API
Java中软引用、弱引用和虚引用的使用方法示例
java强引用,软引用,弱引用,虚引用
JDK源码解析/深入理解Reference和ReferenceQueue
Reference、ReferenceQueue 详解
Java对象的强、软、弱和虚引用原理+结合ReferenceQueue对象构造Java对象的高速缓存器