之前探讨过一次JAVA的FinalReference,这次我们来看下java.lang.ref包下对应的其他三种引用。
走近引用 Reference和ReferenceQueue在使用中一定是结伴出现的,当一个Reference确定要被GC回收,GC便会把Reference加入到与之关联的ReferenceQueue中。注意:在Reference的构造方法中,我们可以传入一个注册队列ReferenceQueue,这个队列我们稍后会具体看,需要主要的是,这个队列需要单独的线程去做消费,否则会存在OOM的隐患。
这些引用可用来实现不同的缓存类型(内存敏感和内存不敏感),大名鼎鼎的Guava cache就是基于引用的这些特性来实现高速本地缓存。
StrongReference(强引用) 我们平时开发中new一个对象出来,这种引用便是强引用。 JVM 系统采用 Finalizer 来管理每个强引用对象 , 并将其被标记要清理时加入 ReferenceQueue, 并逐一调用该对象的 finalize() 方法。具体详见我的前一篇博客:JDK源码 FinalReference
SoftReference(软引用) 当内存足够的时候,软引用所指向的对象没有其他强引用指向的话,GC的时候并不会被回收,当且只当内存不够 时才会被GC回收(调用finalize方法)。强度仅次于强引用。GC回收前,会将那些已经向引用队列注册的新清除的软引用加入队列。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 public class ClassSoft { public static class Referred { @Override protected void finalize () throws Throwable { System.out.println("Referred对象被垃圾收集" ); } @Override public String toString () { return "I am Referred" ; } } public static void collect () throws InterruptedException { System.gc(); Thread.sleep(2000 ); } static class CheckRefQueueThread extends Thread { @Override public void run () { Reference<Referred> obj = null ; try { obj = (Reference<Referred>)softQueue.remove(); } catch (InterruptedException e) { e.printStackTrace(); } if (obj != null ) { try { Field referent = Reference.class.getDeclaredField("referent" ); referent.setAccessible(true ); Object result = referent.get(obj); System.out.println("gc will collect: " + result.getClass() + "@" + result.hashCode()); } catch (Exception e) { e.printStackTrace(); } System.out.println("Object for SoftReference is " + obj.get()); } } } static ReferenceQueue<Referred> softQueue = new ReferenceQueue <>(); public static void main (String[] args) throws InterruptedException { System.out.println("创建软引用" ); Referred strong = new Referred (); SoftReference<Referred> soft = new SoftReference <>(strong,softQueue); new CheckRefQueueThread ().start(); ClassSoft.collect(); System.out.println("切断强引用" ); strong = null ; ClassSoft.collect(); System.out.println("GC之前,软引用值:" + soft.get().toString()); System.out.println("开始堆占用" ); try { List<byte []> bytes = new ArrayList <>(); while (true ) { bytes.add(new byte [1024 *1024 ]); ClassSoft.collect(); } } catch (OutOfMemoryError e) { System.out.println("内存溢出..." ); } System.out.println("Done" ); } }
程序输出如下:
1 2 3 4 5 6 7 8 9 10 创建软引用 切断强引用 GC之前,软引用值:I am Referred 开始堆占用 java.lang.NullPointerException Referred对象被垃圾收集 at com.cxd.jvm.references.ref.ClassSoft$CheckRefQueueThread.run(ClassSoft.java:54) Object for SoftReference is null 内存溢出... Done
我们可以看到,软引用在GC回收前,调用get方法是可以返回其关联的实际对象的,当其被GC加入ReferenceQueue前,JVM会将其关联的对象置为null。
WeakReference(弱引用) 弱引用指向的对象没有任何强引用指向的话,GC的时候会进行回收。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public class ClassWeak { public static class Referred { @Override protected void finalize () throws Throwable { System.out.println("Referred对象被垃圾收集" ); } @Override public String toString () { return "I am weak" ; } } public static void collect () throws InterruptedException { System.gc(); Thread.sleep(2000 ); } public static void main (String[] args) throws InterruptedException { System.out.println("创建一个弱引用" ); Referred strong = new Referred (); WeakReference<Referred> weak = new WeakReference <>(strong); ClassWeak.collect(); System.out.println("切断强引用" ); strong = null ; System.out.println("GC之前,弱引用值:" + weak.get().toString()); ClassWeak.collect(); System.out.println("Done" ); } }
程序输出如下:
1 2 3 4 5 创建一个弱引用 切断强引用 GC之前,弱引用值:I am weak Referred对象被垃圾收集 Done
PhantomReference(虚引用) 如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。这个特性,决定了他的get方法每次调用均会返回null。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 public class ClassPhantom { public static class Referred { @Override protected void finalize () throws Throwable { System.out.println("Referred对象被垃圾收集" ); } @Override public String toString () { return "Referredqq" ; } } public static void collect () throws InterruptedException { System.gc(); Thread.sleep(2000 ); } static class CheckRefQueueThread extends Thread { @Override public void run () { Reference<Referred> obj = null ; try { obj = (Reference<Referred>) phantomQueue.remove(); } catch (InterruptedException e) { e.printStackTrace(); } if (obj != null ) { System.out.println("Object for phantomReference is " + obj.get()); try { Field referent = Reference.class.getDeclaredField("referent" ); referent.setAccessible(true ); Object result = referent.get(obj); System.out.println("gc will collect: " + result.getClass() + "@" + result.toString()); } catch (Exception e) { e.printStackTrace(); } } }; } static ReferenceQueue<Referred> phantomQueue = new ReferenceQueue <>(); public static void main (String[] args) throws InterruptedException { System.out.println("创建一个软引用" ); Referred strong = new Referred (); PhantomReference<Referred> soft = new PhantomReference <>(strong, phantomQueue); new CheckRefQueueThread ().start(); collect(); System.out.println("切断强引用" ); strong = null ; collect(); System.out.println("开始堆占用" ); try { List<byte []> bytes = new ArrayList <>(); while (true ) { bytes.add(new byte [1024 *1024 ]); collect(); } } catch (OutOfMemoryError e) { System.out.println("内存溢出..." ); } System.out.println("Done" ); } }
输出如下:
1 2 3 4 5 6 7 8 创建一个软引用 切断强引用 Referred对象被垃圾收集 开始堆占用 Object for phantomReference is null gc will collect: class com.cxd.jvm.references.ref.ClassPhantom$Referred@Referredqq 内存溢出... Done
引用间的差别 我们注意到虚引用在被加入到ReferenceQueue中后,关联对象并没有被置为null,这点和弱引用及软引用不同。这也是我开头说的潜在OOM的最大风险。当然,这种现象只是加速了OOM问题的暴露,并不是根本原因。JVM GC的这个模型可以看作是生产-消费模型,GC是生产者,我们自己起的线程是消费者(Finalizer中JDK自带线程),当只有生产者时,OOM是迟早的事情。
ReferenceQueue 我们介绍的这四种引用都从java.lang.ref.Reference继承,Reference是个单向链表,ReferenceQueue利用Reference的这个特性来维护先进后出单向队列(类似栈)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 public abstract class Reference <T> { private T referent; ReferenceQueue<? super T> queue; Reference next; } public class ReferenceQueue <T> { private static class Null extends ReferenceQueue { boolean enqueue (Reference r) { return false ; } } static ReferenceQueue NULL = new Null (); static ReferenceQueue ENQUEUED = new Null (); boolean enqueue (Reference<? extends T> r) { synchronized (r) { if (r.queue == ENQUEUED) return false ; synchronized (lock) { 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 ; } } } private Reference<? extends T > reallyPoll() { if (head != null ) { Reference<? extends T > r = head; head = (r.next == r) ? null : r.next; r.queue = NULL; r.next = r; queueLength--; if (r instanceof FinalReference) { sun.misc.VM.addFinalRefCount(-1 ); } return r; } return null ; } }
扩展WeakHashMap JDK中有对引用的具体使用,当我们需要实现一个简单的本地内存敏感缓存时,可以考虑使用WeakHashMap,此处不再分析其源码。WeakHashMap的每个Entry都是WeakReference的子类,每次put或者get或者resize扩容时,都会调用WeakHashMap的expungeStaleEntries方法,清除那些被GC加入到ReferenceQueue的Entry。
小生不才,以上如有描述有误的地方还望各位不吝赐教 !^_^!