Java Reference 原理

注意,以下所述源码版本为 JDK 1.8.0_212 

 

1 引用的概念

Java中的数据类型分为:

基本数据类型:byte、short、int、long、float、double、char、boolean 8种。

引用类型:上述基本数据类型的包装类、其他各种对象类型。如Integer、Object等。

当说到“引用”时,指的可能是 引用类型 或 一个引用类型的变量,具体视上下文而定。

 

在JDK1.2之前,Java中的引用的定义是十分传统的:如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。在这种定义之下,一个对象只有被引用和没有被引用两种状态,用户代码中无法在对象被GC回收后做一些额外的工作(如清理堆外内存等)。

实际上,我们更希望存在这样的一类对象:当内存空间还足够的时候,这些对象能够保留在内存空间中;如果当内存空间在进行了垃圾收集之后还是非常紧张,则可以回收这些对象。基于这种特性,可以满足很多系统的例如缓存功能这样的使用场景。

从JDK 1.2起,对引用概念进行了扩充,将引用分为 强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)、终引用(Final Reference)。其中强引用就是JDK 1.2之前的引用,日常代码中绝大多数引用都是强引用;而其他几种引用则是JDK 1.2引进的,通过这些新引入的引用可以达到两个主要效果

1 即使被引用的对象也可能被GC回收;

2 有被引用的对象却被回收了,如果不通过某种手段让用户可感知,那么程序很容易有莫名其妙的逻辑问题。故对象被回收时要让在用户代码中可感知到从而可进行一些额处理工作。

新引入的引用是java.lang.Reference抽象类的子类(强引用则不是),其间的关系如下:

注:

强引用没有对应的类型表示,也就是说强引用是普遍存在的,如Object object = new Object();  中的object就是强引用。

直接继承java.lang.ref.Reference创建自定义的引用类型是无效的,但是可以直接继承已经存在的引用类型,如sun.misc.Cleaner就是继承自java.lang.ref.PhantomReference。 

FinalReference 是package private的作用域的,因此 FinalReference、Finalizer 都无法在用户代码中直接使用,而是由JVM去创建的

Cleaner位于 sun.misc 包下而非 java.lang.ref 。

 

2 引用的创建

直接定义的引用变量是强引用,如 Integer a = new Integer()  ,a是强引用。

其他几种引用则需要通过Reference子类创建,如 WeakReference<Integer> wr = new WeakReference<>(a); 。这里wr仍是强引用,wr对象内的referent成员才是弱引用。

可见,在代码书写上,各种引用的写法是一样的,只是概念上的区分(wr内部的也是 private T referent; referent=a; 这样与强引用无异的写法)。那如何让不同的引用有不同的效果呢?对于未被任何变量引用的对象,JVM会回收;而对于有被引用的对象,JVM通过判断该对象是否只被Refrence对象引用来确认是否回收它,且通过引用它的Reference对象类型来判断是软/弱/虚的哪种引用。

3 引用对象的可达性及回收

对象间互相引用形成引用链。不同引用间的强弱关系依次是 强引用 > 软引用 > 弱引用 > 虚引用 > 终引用,如果有更强的引用关系存在,那么引用链的可达性将由更强的引用关系决定。因此,可达性就分为:

强可达:对象与GC Root间存在强引用链。例如下图的A、B。

软可达:对象与GC Root间不存在强引用链,但存在软引用链。例如下图的E。

弱可达:对象与GC Root间不存在强、软引用链,但存在弱引用链。例如下图的F、C、D。

虚可达:对象与GC Root间不存在强、软、弱引用链,但存在虚引用链。例如下图的G。

不可达:对象与GC Root间不存在任何引用链。例如下图的H、I。

强引用(日常代码里的引用类型变量几乎都是这种引用)的对象不会被回收(严格来说也不是绝对的,如软、弱、虚引用对象内部对实际对象的引用referent也是强引用,但说强引用时一般不包括此情形),其他引用对象则可能被回收。

软引用:软引用对象在系统将要发生内存耗尽(OOM)前会被回收。示例:

 1 // VM参数:-Xmx4m -Xms4m
 2 public class SoftReferenceMain {
 3 
 4     public static void main(String[] args) throws Exception {
 5         ReferenceQueue<SoftReferenceObject> queue = new ReferenceQueue<>();
 6         SoftReferenceObject object = new SoftReferenceObject();
 7         SoftReference<SoftReferenceObject> reference = new SoftReference<>(object, queue);
 8         object = null;
 9         System.gc();
10         Thread.sleep(500);
11         System.out.println(reference.get());
12     }
13 
14     private static class SoftReferenceObject {
15 
16         int[] array = new int[120_000];
17 
18         @Override
19         public String toString() {
20             return "SoftReferenceObject";
21         }
22     }
23 }
24 // 运行后输出结果,可见GC后软引用关联的对象被回收了
25 null
SoftReferenceMain

弱引用:一旦发生GC必定回收被弱引用关联的对象,不管当前的内存是否足够。其一个特点是它何时被回收是不可确定的,因为这是由GC运行的不确定性所确定的。示例:

 1 class WeakReferenceMain {
 2 
 3     public static void main(String[] args) throws Exception {
 4         ReferenceQueue<WeakReferenceObject> queue = new ReferenceQueue<>();
 5         WeakReferenceObject object = new WeakReferenceObject();
 6         System.out.println(object);
 7         WeakReference<WeakReferenceObject> reference = new WeakReference<>(object, queue);
 8         object = null;
 9         Thread.sleep(500);
10         System.out.println(reference.get());
11         System.gc();
12         Thread.sleep(500);
13         System.out.println(reference.get());
14     }
15 
16     private static class WeakReferenceObject {
17 
18         @Override
19         public String toString() {
20             return "WeakReferenceObject";
21         }
22     }
23 }
24 
25 // 运行后输出结果
26 WeakReferenceObject
27 WeakReferenceObject
28 null
WeakReferenceMain

虚引用:虚引用对象肯定会被回收,一个对象是否有虚引用完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例(PhantomReference覆盖了Reference#get()并且总是返回null),对象被设置成虚引用关联的唯一作用是在对象被垃圾收集时收到一个系统通知。JDK里的实现是 sun.misc.Cleaner。示例见后文的DireactByteBuffer。

终引用:对象被回收时由JVM包装成这种引用。当对象重写了 Object#finalize 方法、该方法未被调用过、方法内部该对象又被成员变量(类变量或实例变量)引用,则对象不被回收,否则被回收。finalize最多只会被JVM调用一次。JDK里的实现是Finalizer。

总结:

 

 

4 Reference核心原理

(注:以下所述的Reference类型、引用类型指的是Reference各种子类的引用类型,因此强引用类型不包括在内

这里的核心原理也即上面说的“引入Reference所带来的两种效果”是如何实现的,即:

1 JVM怎么做到对被引用了的对象还去回收它的?这点很简单,若对象只被Reference对象引用则可能被回收、且根据Reference对象类型可知道是哪种引用,从而决定相应的处理操作。

2 如何让被引用的对象被回收时让用户代码可感知到?核心逻辑在Reference类中(通过全局pending变量、守护线程、引用队列等配合实现),这正是本节的内容,下述。

由于新引进的几种引用类型是通过Reference子类创建的,该子类对象自身就是一个对象、该对象内部包含对实际对象的引用。故为免描述上的歧义,这里明确下概念:

“Reference对象”指Reference类或其子类变量直接指向的对象。如前面的wr指向的对象。

“引用对象”指 Reference变量对应的最终对象。如前面的a。 wr指向的对象内部包含对实际对象的引用,可通过wr.get()获取该实际对象。

4.1 出发点

引入各种新引用类型的主要作用是 使得用户代码层面能够知道哪些对象被JVM GC回收了 及 能够在知道对象被回收时做一些额外的处理工作。这里权且名之曰“对象回收感知”

 

4.2 相关类

Reference:抽象类,前面介绍的JDK 1.2扩充的软、弱、虚引用都是该类的子类,这些扩充的引用类型的数据结构及实现上述“出发点”的核心逻辑都在该抽象类中实现了。相关源码:

  1 /**
  2  * Abstract base class for reference objects.  This class defines the
  3  * operations common to all reference objects.  Because reference objects are
  4  * implemented in close cooperation with the garbage collector, this class may
  5  * not be subclassed directly.
  6  *
  7  * @author   Mark Reinhold
  8  * @since    1.2
  9  */
 10 
 11 public abstract class Reference<T> {
 12 
 13     /* A Reference instance is in one of four possible internal states:
 14      *
 15      *     Active: Subject to special treatment by the garbage collector.  Some
 16      *     time after the collector detects that the reachability of the
 17      *     referent has changed to the appropriate state, it changes the
 18      *     instance's state to either Pending or Inactive, depending upon
 19      *     whether or not the instance was registered with a queue when it was
 20      *     created.  In the former case it also adds the instance to the
 21      *     pending-Reference list.  Newly-created instances are Active.
 22      *
 23      *     Pending: An element of the pending-Reference list, waiting to be
 24      *     enqueued by the Reference-handler thread.  Unregistered instances
 25      *     are never in this state.
 26      *
 27      *     Enqueued: An element of the queue with which the instance was
 28      *     registered when it was created.  When an instance is removed from
 29      *     its ReferenceQueue, it is made Inactive.  Unregistered instances are
 30      *     never in this state.
 31      *
 32      *     Inactive: Nothing more to do.  Once an instance becomes Inactive its
 33      *     state will never change again.
 34      *
 35      * The state is encoded in the queue and next fields as follows:
 36      *
 37      *     Active: queue = ReferenceQueue with which instance is registered, or
 38      *     ReferenceQueue.NULL if it was not registered with a queue; next =
 39      *     null.
 40      *
 41      *     Pending: queue = ReferenceQueue with which instance is registered;
 42      *     next = this
 43      *
 44      *     Enqueued: queue = ReferenceQueue.ENQUEUED; next = Following instance
 45      *     in queue, or this if at end of list.
 46      *
 47      *     Inactive: queue = ReferenceQueue.NULL; next = this.
 48      *
 49      * With this scheme the collector need only examine the next field in order
 50      * to determine whether a Reference instance requires special treatment: If
 51      * the next field is null then the instance is active; if it is non-null,
 52      * then the collector should treat the instance normally.
 53      *
 54      * To ensure that a concurrent collector can discover active Reference
 55      * objects without interfering with application threads that may apply
 56      * the enqueue() method to those objects, collectors should link
 57      * discovered objects through the discovered field. The discovered
 58      * field is also used for linking Reference objects in the pending list.
 59      */
 60 
 61     private T referent;         /* Treated specially by GC */
 62 
 63     volatile ReferenceQueue<? super T> queue;
 64 
 65     /* When active:   NULL
 66      *     pending:   this
 67      *    Enqueued:   next reference in queue (or this if last)
 68      *    Inactive:   this
 69      */
 70     @SuppressWarnings("rawtypes")
 71     volatile Reference next;
 72 
 73     /* When active:   next element in a discovered reference list maintained by GC (or this if last)
 74      *     pending:   next element in the pending list (or null if last)
 75      *   otherwise:   NULL
 76      */
 77     transient private Reference<T> discovered;  /* used by VM */
 78 
 79 
 80     /* Object used to synchronize with the garbage collector.  The collector
 81      * must acquire this lock at the beginning of each collection cycle.  It is
 82      * therefore critical that any code holding this lock complete as quickly
 83      * as possible, allocate no new objects, and avoid calling user code.
 84      */
 85     static private class Lock { }
 86     private static Lock lock = new Lock();
 87 
 88 
 89     /* List of References waiting to be enqueued.  The collector adds
 90      * References to this list, while the Reference-handler thread removes
 91      * them.  This list is protected by the above lock object. The
 92      * list uses the discovered field to link its elements.
 93      */
 94     private static Reference<Object> pending = null;
 95 
 96     /* High-priority thread to enqueue pending References
 97      */
 98     private static class ReferenceHandler extends Thread {
 99 
100         private static void ensureClassInitialized(Class<?> clazz) {
101             try {
102                 Class.forName(clazz.getName(), true, clazz.getClassLoader());
103             } catch (ClassNotFoundException e) {
104                 throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
105             }
106         }
107 
108         static {
109             // pre-load and initialize InterruptedException and Cleaner classes
110             // so that we don't get into trouble later in the run loop if there's
111             // memory shortage while loading/initializing them lazily.
112             ensureClassInitialized(InterruptedException.class);
113             ensureClassInitialized(Cleaner.class);
114         }
115 
116         ReferenceHandler(ThreadGroup g, String name) {
117             super(g, name);
118         }
119 
120         public void run() {
121             while (true) {
122                 tryHandlePending(true);
123             }
124         }
125     }
126 
127     /**
128      * Try handle pending {@link Reference} if there is one.<p>
129      * Return {@code true} as a hint that there might be another
130      * {@link Reference} pending or {@code false} when there are no more pending
131      * {@link Reference}s at the moment and the program can do some other
132      * useful work instead of looping.
133      *
134      * @param waitForNotify if {@code true} and there was no pending
135      *                      {@link Reference}, wait until notified from VM
136      *                      or interrupted; if {@code false}, return immediately
137      *                      when there is no pending {@link Reference}.
138      * @return {@code true} if there was a {@link Reference} pending and it
139      *         was processed, or we waited for notification and either got it
140      *         or thread was interrupted before being notified;
141      *         {@code false} otherwise.
142      */
143     static boolean tryHandlePending(boolean waitForNotify) {
144         Reference<Object> r;
145         Cleaner c;
146         try {
147             synchronized (lock) {
148                 if (pending != null) {
149                     r = pending;
150                     // 'instanceof' might throw OutOfMemoryError sometimes
151                     // so do this before un-linking 'r' from the 'pending' chain...
152                     c = r instanceof Cleaner ? (Cleaner) r : null;
153                     // unlink 'r' from 'pending' chain
154                     pending = r.discovered;
155                     r.discovered = null;
156                 } else {
157                     // The waiting on the lock may cause an OutOfMemoryError
158                     // because it may try to allocate exception objects.
159                     if (waitForNotify) {
160                         lock.wait();
161                     }
162                     // retry if waited
163                     return waitForNotify;
164                 }
165             }
166         } catch (OutOfMemoryError x) {
167             // Give other threads CPU time so they hopefully drop some live references
168             // and GC reclaims some space.
169             // Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above
170             // persistently throws OOME for some time...
171             Thread.yield();
172             // retry
173             return true;
174         } catch (InterruptedException x) {
175             // retry
176             return true;
177         }
178 
179         // Fast path for cleaners
180         if (c != null) {
181             c.clean();
182             return true;
183         }
184 
185         ReferenceQueue<? super Object> q = r.queue;
186         if (q != ReferenceQueue.NULL) q.enqueue(r);
187         return true;
188     }
189 
190     static {
191         ThreadGroup tg = Thread.currentThread().getThreadGroup();
192         for (ThreadGroup tgn = tg;
193              tgn != null;
194              tg = tgn, tgn = tg.getParent());
195         Thread handler = new ReferenceHandler(tg, "Reference Handler");
196         /* If there were a special system-only priority greater than
197          * MAX_PRIORITY, it would be used here
198          */
199         handler.setPriority(Thread.MAX_PRIORITY);
200         handler.setDaemon(true);
201         handler.start();
202 
203         // provide access in SharedSecrets
204         SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
205             @Override
206             public boolean tryHandlePendingReference() {
207                 return tryHandlePending(false);
208             }
209         });
210     }
211 
212     /* -- Referent accessor and setters -- */
213 
214     /**
215      * Returns this reference object's referent.  If this reference object has
216      * been cleared, either by the program or by the garbage collector, then
217      * this method returns <code>null</code>.
218      *
219      * @return   The object to which this reference refers, or
220      *           <code>null</code> if this reference object has been cleared
221      */
222     public T get() {
223         return this.referent;
224     }
225 
226     /**
227      * Clears this reference object.  Invoking this method will not cause this
228      * object to be enqueued.
229      *
230      * <p> This method is invoked only by Java code; when the garbage collector
231      * clears references it does so directly, without invoking this method.
232      */
233     public void clear() {
234         this.referent = null;
235     }
236 
237 
238     /* -- Queue operations -- */
239 
240     /**
241      * Tells whether or not this reference object has been enqueued, either by
242      * the program or by the garbage collector.  If this reference object was
243      * not registered with a queue when it was created, then this method will
244      * always return <code>false</code>.
245      *
246      * @return   <code>true</code> if and only if this reference object has
247      *           been enqueued
248      */
249     public boolean isEnqueued() {
250         return (this.queue == ReferenceQueue.ENQUEUED);
251     }
252 
253     /**
254      * Adds this reference object to the queue with which it is registered,
255      * if any.
256      *
257      * <p> This method is invoked only by Java code; when the garbage collector
258      * enqueues references it does so directly, without invoking this method.
259      *
260      * @return   <code>true</code> if this reference object was successfully
261      *           enqueued; <code>false</code> if it was already enqueued or if
262      *           it was not registered with a queue when it was created
263      */
264     public boolean enqueue() {
265         return this.queue.enqueue(this);
266     }
267 
268 
269     /* -- Constructors -- */
270 
271     Reference(T referent) {
272         this(referent, null);
273     }
274 
275     Reference(T referent, ReferenceQueue<? super T> queue) {
276         this.referent = referent;
277         this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
278     }
279 
280 }
abstract class Reference

Reference类的关键成员变量有两个:referent、queue。前者表示该“Reference对象”的最终引用的对象,后者表示队列(下面介绍)。有两个构造方法,一个是只有referent参数、一个是还有queue参数,可见:queue是由Reference对象的创建者提供;一个对象可以被不同的Reference对象关联。

Reference类还有个关键的类变量:private static Reference<Object> pending,由JVM GC负责为该变量赋值——在referent对象被回收时对应的Reference对象会被JVM GC赋给pending;下面将介绍的ReferenceHandler会从pending取值做进一步处理,可见 pending 是JVM GC逻辑与用户代码逻辑交互的媒介。(pending与下面的ReferenceHandler一样是Reference类的静态内容,感觉与Reference自身的数据结构关系不大,因此不应耦合在Reference类中而是应拆到单独类里实现?)

ReferenceHandler:Reference类的静态内部类,是Thread的子类。

Reference类内部static代码块中会启动该线程,该线程优先级是最高级别的且是守护线程。可见,对于一个Java程序而言,只要Reference类被JVM加载后就会有该线程存在。

该线程做的事是:将被JVM GC回收的对象对应的Reference对象取出(通过Reference类的pending成员变量取,JVM在回收对象时会将对应的Reference对象存到该变量中) ,执行其clean方法(虚引用场景)或放到ReferenceQueue队列中(软、弱引用场景)。相关源码:

 1     static {
 2         ThreadGroup tg = Thread.currentThread().getThreadGroup();
 3         for (ThreadGroup tgn = tg;
 4              tgn != null;
 5              tg = tgn, tgn = tg.getParent());
 6         Thread handler = new ReferenceHandler(tg, "Reference Handler");
 7         /* If there were a special system-only priority greater than
 8          * MAX_PRIORITY, it would be used here
 9          */
10         handler.setPriority(Thread.MAX_PRIORITY);
11         handler.setDaemon(true);
12         handler.start();
13 
14         // provide access in SharedSecrets
15         SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
16             @Override
17             public boolean tryHandlePendingReference() {
18                 return tryHandlePending(false);
19             }
20         });
21     }
22 
23 
24     static boolean tryHandlePending(boolean waitForNotify) {
25         Reference<Object> r;
26         Cleaner c;
27         try {
28             synchronized (lock) {
29                 if (pending != null) {
30                     r = pending;
31                     // 'instanceof' might throw OutOfMemoryError sometimes
32                     // so do this before un-linking 'r' from the 'pending' chain...
33                     c = r instanceof Cleaner ? (Cleaner) r : null;
34                     // unlink 'r' from 'pending' chain
35                     pending = r.discovered;
36                     r.discovered = null;
37                 } else {
38                     // The waiting on the lock may cause an OutOfMemoryError
39                     // because it may try to allocate exception objects.
40                     if (waitForNotify) {
41                         lock.wait();
42                     }
43                     // retry if waited
44                     return waitForNotify;
45                 }
46             }
47         } catch (OutOfMemoryError x) {
48             // Give other threads CPU time so they hopefully drop some live references
49             // and GC reclaims some space.
50             // Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above
51             // persistently throws OOME for some time...
52             Thread.yield();
53             // retry
54             return true;
55         } catch (InterruptedException x) {
56             // retry
57             return true;
58         }
59 
60         // Fast path for cleaners
61         if (c != null) {
62             c.clean();
63             return true;
64         }
65 
66         ReferenceQueue<? super Object> q = r.queue;
67         if (q != ReferenceQueue.NULL) q.enqueue(r);
68         return true;
69     }
View Code

ReferenceQueue:队列,用来存放“被垃圾回收的对象”对应的Reference子类对象,如SoftReference、WeakReference对象等。

该队列只存储了引用链表的头节点,提供了引用链表的操作,实际上,引用链表是Reference实例内部变量存储。

通常让同一个队列作为多个Reference对象的构造参数,这些Reference对象对应的实际对象可以是有交集(即一个“引用对象”对应多个“Reference对象”的情形)或无交集的。

从上面可知,JVM GC在回收对象时该对象对应的Reference对象可能会(构造时有提供queue参数)被ReferenceHandler放入队列。因此,用户程序通过拉取queue元素可知哪些对象被回收了,从而做一些个性化处理。WeakHashMap就是借助此特点这实现的

 

4.3 核心流程

在GC时如果当前对象只被Reference对象引用(虽然这引用也是强引用),JVM会根据Reference具体类型决定是否把当前对象相关的Reference对象加入到一个Reference类型的pending链表上,如果能加入pending链表JVM同时会通知ReferenceHandler线程进行处理。ReferenceHandler线程收到通知后会调用Cleaner#clean或ReferenceQueue#enqueue方法进行处理。用户程序就可以通过从该队列拉取元素得到被回收的对象对应的Reference对象。流程图如下:

 

4.4 各引用的原理

这里的引用指JDK中 Reference 类的各种子类实现。

总的来说,SoftReference、WeakReference、PhantomReference、FinalReference、Finalizer 在实现上核心逻辑与Reference中的一样(从源码就能看出来,几乎都是委托给Reference类),只有Finalizer 更特殊点,其把“感知对象回收、做额外处理”的事情也做了而非像 WeakReference 等那样由用户代码自行去做。

既然前四者逻辑几乎与Reference一样,为何要有这么多种不同的类型、为何不直接采用Reference类?因为这相当于一种标识或约定,JVM会根据对象关联的Reference类型的不同来对对象做不同处理

若当前对象仅被SoftReference对象引用,则JVM在要OOM时才会回收当前对象;

若当前对象仅被WeakReference对象引用,则JVM在当前对象遇到第二次垃圾回收时会回收之;

若当前对象仅被PhantomReference/Cleaner对象引用,则JVM肯定会回收当前对象;

若当前对象被回收则会被JVM包装成 FinalReference/Finalizer 引用,进一步地,JVM会根据当前对象finalize方法是否重写及是否被调用过给与其一次逃过被回收的机会。

 

SoftReference

核心逻辑可认为与Reference的一样,只不过加了tiimestamp实例变量,从源码可见:

 1 public class SoftReference<T> extends Reference<T> {
 2 
 3     /**
 4      * Timestamp clock, updated by the garbage collector
 5      */
 6     static private long clock;
 7 
 8     /**
 9      * Timestamp updated by each invocation of the get method.  The VM may use
10      * this field when selecting soft references to be cleared, but it is not
11      * required to do so.
12      */
13     private long timestamp;
14 
15     /**
16      * Creates a new soft reference that refers to the given object.  The new
17      * reference is not registered with any queue.
18      *
19      * @param referent object the new soft reference will refer to
20      */
21     public SoftReference(T referent) {
22         super(referent);
23         this.timestamp = clock;
24     }
25 
26     /**
27      * Creates a new soft reference that refers to the given object and is
28      * registered with the given queue.
29      *
30      * @param referent object the new soft reference will refer to
31      * @param q the queue with which the reference is to be registered,
32      *          or <tt>null</tt> if registration is not required
33      *
34      */
35     public SoftReference(T referent, ReferenceQueue<? super T> q) {
36         super(referent, q);
37         this.timestamp = clock;
38     }
39 
40     /**
41      * Returns this reference object's referent.  If this reference object has
42      * been cleared, either by the program or by the garbage collector, then
43      * this method returns <code>null</code>.
44      *
45      * @return   The object to which this reference refers, or
46      *           <code>null</code> if this reference object has been cleared
47      */
48     public T get() {
49         T o = super.get();
50         if (o != null && this.timestamp != clock)
51             this.timestamp = clock;
52         return o;
53     }
54 
55 }
SoftReference

 

WeakReference

核心逻辑可与Reference的一样,从源码可见:

 1 public class WeakReference<T> extends Reference<T> {
 2 
 3     /**
 4      * Creates a new weak reference that refers to the given object.  The new
 5      * reference is not registered with any queue.
 6      *
 7      * @param referent object the new weak reference will refer to
 8      */
 9     public WeakReference(T referent) {
10         super(referent);
11     }
12 
13     /**
14      * Creates a new weak reference that refers to the given object and is
15      * registered with the given queue.
16      *
17      * @param referent object the new weak reference will refer to
18      * @param q the queue with which the reference is to be registered,
19      *          or <tt>null</tt> if registration is not required
20      */
21     public WeakReference(T referent, ReferenceQueue<? super T> q) {
22         super(referent, q);
23     }
24 
25 }
WeakReference

 

PhantomReference、Cleaner

前者核心逻辑与Reference的一样,从源码可见:

 1 public class PhantomReference<T> extends Reference<T> {
 2 
 3     /**
 4      * Returns this reference object's referent.  Because the referent of a
 5      * phantom reference is always inaccessible, this method always returns
 6      * <code>null</code>.
 7      *
 8      * @return  <code>null</code>
 9      */
10     public T get() {
11         return null;
12     }
13 
14     /**
15      * Creates a new phantom reference that refers to the given object and
16      * is registered with the given queue.
17      *
18      * <p> It is possible to create a phantom reference with a <tt>null</tt>
19      * queue, but such a reference is completely useless: Its <tt>get</tt>
20      * method will always return null and, since it does not have a queue, it
21      * will never be enqueued.
22      *
23      * @param referent the object the new phantom reference will refer to
24      * @param q the queue with which the reference is to be registered,
25      *          or <tt>null</tt> if registration is not required
26      */
27     public PhantomReference(T referent, ReferenceQueue<? super T> q) {
28         super(referent, q);
29     }
30 
31 }
PhantomReference

后者增加了 clean 回调方法,被 ReferenceHandler 调用(见前面Reference原理部分)。源码在 sun.misc 包中。

JDK 9引入了java.lang.ref.Cleaner,且提倡用Cleaner来替换 Object#finalize() 功能。

Cleaner中用全局变量来保存链表头结点,从而保存了要被clean的虚引用对象链表。

 

FinalReference、Finalizer

前者核心逻辑与Reference的一样,只不过创建FinalReference时要求一定要提供队列参数,从源码可见:

1 class FinalReference<T> extends Reference<T> {
2 
3     public FinalReference(T referent, ReferenceQueue<? super T> q) {
4         super(referent, q);
5     }
6 }
FinalReference

后者基于Reference核心流程做了修改或扩充,源码:

  1 final class Finalizer extends FinalReference<Object> { /* Package-private; must be in
  2                                                           same package as the Reference
  3                                                           class */
  4 
  5     private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
  6     private static Finalizer unfinalized = null;
  7     private static final Object lock = new Object();
  8 
  9     private Finalizer
 10         next = null,
 11         prev = null;
 12 
 13     private boolean hasBeenFinalized() {
 14         return (next == this);
 15     }
 16 
 17     private void add() {
 18         synchronized (lock) {
 19             if (unfinalized != null) {
 20                 this.next = unfinalized;
 21                 unfinalized.prev = this;
 22             }
 23             unfinalized = this;
 24         }
 25     }
 26 
 27     private void remove() {
 28         synchronized (lock) {
 29             if (unfinalized == this) {
 30                 if (this.next != null) {
 31                     unfinalized = this.next;
 32                 } else {
 33                     unfinalized = this.prev;
 34                 }
 35             }
 36             if (this.next != null) {
 37                 this.next.prev = this.prev;
 38             }
 39             if (this.prev != null) {
 40                 this.prev.next = this.next;
 41             }
 42             this.next = this;   /* Indicates that this has been finalized */
 43             this.prev = this;
 44         }
 45     }
 46 
 47     private Finalizer(Object finalizee) {
 48         super(finalizee, queue);
 49         add();
 50     }
 51 
 52     static ReferenceQueue<Object> getQueue() {
 53         return queue;
 54     }
 55 
 56     /* Invoked by VM */
 57     static void register(Object finalizee) {
 58         new Finalizer(finalizee);
 59     }
 60 
 61     private void runFinalizer(JavaLangAccess jla) {
 62         synchronized (this) {
 63             if (hasBeenFinalized()) return;
 64             remove();
 65         }
 66         try {
 67             Object finalizee = this.get();
 68             if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
 69                 jla.invokeFinalize(finalizee);
 70 
 71                 /* Clear stack slot containing this variable, to decrease
 72                    the chances of false retention with a conservative GC */
 73                 finalizee = null;
 74             }
 75         } catch (Throwable x) { }
 76         super.clear();
 77     }
 78 
 79     /* Create a privileged secondary finalizer thread in the system thread
 80        group for the given Runnable, and wait for it to complete.
 81 
 82        This method is used by both runFinalization and runFinalizersOnExit.
 83        The former method invokes all pending finalizers, while the latter
 84        invokes all uninvoked finalizers if on-exit finalization has been
 85        enabled.
 86 
 87        These two methods could have been implemented by offloading their work
 88        to the regular finalizer thread and waiting for that thread to finish.
 89        The advantage of creating a fresh thread, however, is that it insulates
 90        invokers of these methods from a stalled or deadlocked finalizer thread.
 91      */
 92     private static void forkSecondaryFinalizer(final Runnable proc) {
 93         AccessController.doPrivileged(
 94             new PrivilegedAction<Void>() {
 95                 public Void run() {
 96                     ThreadGroup tg = Thread.currentThread().getThreadGroup();
 97                     for (ThreadGroup tgn = tg;
 98                          tgn != null;
 99                          tg = tgn, tgn = tg.getParent());
100                     Thread sft = new Thread(tg, proc, "Secondary finalizer");
101                     sft.start();
102                     try {
103                         sft.join();
104                     } catch (InterruptedException x) {
105                         Thread.currentThread().interrupt();
106                     }
107                     return null;
108                 }});
109     }
110 
111     /* Called by Runtime.runFinalization() */
112     static void runFinalization() {
113         if (!VM.isBooted()) {
114             return;
115         }
116 
117         forkSecondaryFinalizer(new Runnable() {
118             private volatile boolean running;
119             public void run() {
120                 // in case of recursive call to run()
121                 if (running)
122                     return;
123                 final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
124                 running = true;
125                 for (;;) {
126                     Finalizer f = (Finalizer)queue.poll();
127                     if (f == null) break;
128                     f.runFinalizer(jla);
129                 }
130             }
131         });
132     }
133 
134     /* Invoked by java.lang.Shutdown */
135     static void runAllFinalizers() {
136         if (!VM.isBooted()) {
137             return;
138         }
139 
140         forkSecondaryFinalizer(new Runnable() {
141             private volatile boolean running;
142             public void run() {
143                 // in case of recursive call to run()
144                 if (running)
145                     return;
146                 final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
147                 running = true;
148                 for (;;) {
149                     Finalizer f;
150                     synchronized (lock) {
151                         f = unfinalized;
152                         if (f == null) break;
153                         unfinalized = f.next;
154                     }
155                     f.runFinalizer(jla);
156                 }}});
157     }
158 
159     private static class FinalizerThread extends Thread {
160         private volatile boolean running;
161         FinalizerThread(ThreadGroup g) {
162             super(g, "Finalizer");
163         }
164         public void run() {
165             // in case of recursive call to run()
166             if (running)
167                 return;
168 
169             // Finalizer thread starts before System.initializeSystemClass
170             // is called.  Wait until JavaLangAccess is available
171             while (!VM.isBooted()) {
172                 // delay until VM completes initialization
173                 try {
174                     VM.awaitBooted();
175                 } catch (InterruptedException x) {
176                     // ignore and continue
177                 }
178             }
179             final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
180             running = true;
181             for (;;) {
182                 try {
183                     Finalizer f = (Finalizer)queue.remove();
184                     f.runFinalizer(jla);
185                 } catch (InterruptedException x) {
186                     // ignore and continue
187                 }
188             }
189         }
190     }
191 
192     static {
193         ThreadGroup tg = Thread.currentThread().getThreadGroup();
194         for (ThreadGroup tgn = tg;
195              tgn != null;
196              tg = tgn, tgn = tg.getParent());
197         Thread finalizer = new FinalizerThread(tg);
198         finalizer.setPriority(Thread.MAX_PRIORITY - 2);
199         finalizer.setDaemon(true);
200         finalizer.start();
201     }
202 
203 }
Finalizer

体现在:(详情可参阅 Finalizer

Finalizer 类是package private 作用域的,构造器是private的,故Finalizer对象只能由JVM创建 :  /* Invoked by VM */ static void register(Object finalizee) { new Finalizer(finalizee); } ;

当数据对象(如 Person 对象)被回收时,在满足条件时会被JVM通过上述方法包装成Finalizer对象注册到队列,条件是:数据对象重写了finalize方法且尚未被调用过

Finalizer 类内部包含了“感知对象回收、根据队列做额外处理”的逻辑(Reference、WeakReference等中不包含这些逻辑,需要由用户实现):在Reference内部逻辑的基础上,Finalizer 类内部还会启动一个线程,从队列中取 Finalizer 对象,调用对象的finalize方法:Finalizer#runFinalizer 。

 Finalizer守护线程是由Finalizer类的静态代码块创建和运行的,作用是处理Finalizer类内部维护的F-Queue链表(链表元素入队操作由JVM实现)的元素调用关联对象的finalize()方法。

ReferenceHandler守护线程线和Finalizer守护线程共同协作才能使引用类型对象内存回收系统的工作能够正常进行。

 

4.5 应用

基于Reference类型的“对象回收感知”特点,有三个典型应用:堆外内存自动回收、WeakHashMap实现不用数据自动回收、ThreadLocalMap解决MemoryLeak。

(这些典型应用好像通过重写 Object#finalize 方法也能实现?)

4.5.1 DirectByteBuffer堆外内存回收

Cleaner是PhantomReference类型的引用。当JVM GC时如果发现当前处理的对象只被PhantomReference类型对象引用,同之前说的一样其会将该Reference对象加到pending-Reference链上,只是ReferenceHandler线程在处理时如果PhantomReference类型实际类型又是Cleaner的话,就会调用Cleaner.clean方法。DirectByteBuffer分配的堆外内存收回就是通过实现Cleaner.clean方法实现的。

创建DirectByteBuffer对象时会创建一个Cleaner对象,Cleaner对象持有了DirectByteBuffer对象的引用。当JVM在GC时,如果发现DirectByteBuffer没被强引用(仅被Reference引用),JVM会将其对应的Cleaner加入到pending-reference链表中,同时通知ReferenceHandler线程处理,ReferenceHandler收到通知后,会调用Cleaner#clean方法,而对于DirectByteBuffer创建的Cleaner对象其clean方法内部会调用unsafe.freeMemory释放堆外内存。最终达到了DirectByteBuffer对象被GC回收时其对应的堆外内存也被回收的目的。相关源码:

 1 // ByteBuffer#allocateDirect
 2 
 3 //直接new一个指定字节大小的DirectByteBuffer对象
 4 public static ByteBuffer allocateDirect(int capacity) {
 5     return new DirectByteBuffer(capacity);
 6 }
 7 DirectByteBuffer(int cap) {
 8     //省略部分代码...
 9     try {
10         //调用unsafe分配内存
11         base = unsafe.allocateMemory(size);
12     } catch (OutOfMemoryError x) {
13        //省略部分代码...
14     }
15     //省略部分代码...
16     //前面分析中的Cleaner对象创建,持有当前DirectByteBuffer的引用
17     cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
18     att = null;
19 }
20 
21 
22 
23 
24 // Deallocator#run
25 public void run() {
26     if (address == 0) {
27         return;
28     }
29     //通过unsafe.freeMemory释放创建的堆外内存
30     unsafe.freeMemory(address);
31     address = 0;
32     Bits.unreserveMemory(size, capacity);
33 }
ByteBuffer#allocateDirect Deallocator#run

 

4.5.2 WeakHashMap实现不用数据的自动回收

WeakHashMap在使用方法上与普通的HashMap一样,区别在于当key不再被使用时(即不强可达时),WeakHashMap中这些key及其对应的entry、value都会被GC回收。其实现上就是借助于WeakReference。

WeakHashMap的Entry继承了WeakReference类,其key相当于前面介绍的Reference类中的referent。在创建Entry时还传了个queue实参、该queue被各Entry共享。相关源码:

 1 //Entry继承了WeakReference, WeakReference引用的是Map的key
 2  private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
 3     V value;
 4     final int hash;
 5     Entry<K,V> next;
 6     /**
 7      * 创建Entry对象,上面分析过的ReferenceQueue,这个queue实际是WeakHashMap的成员变量,
 8      * 创建WeakHashMap时其便被初始化 final ReferenceQueue<Object> queue = new ReferenceQueue<>()
 9      */
10     Entry(Object key, V value,
11           ReferenceQueue<Object> queue,
12           int hash, Entry<K,V> next) {
13         super(key, queue);
14         this.value = value;
15         this.hash  = hash;
16         this.next  = next;
17     }
18     //省略部分原码...
19 }
WeakHashMap Entry
 1 private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
 2 
 3     public V put(K key, V value) {
 4         Object k = maskNull(key);
 5         int h = hash(k);
 6         Entry<K,V>[] tab = getTable();
 7         int i = indexFor(h, tab.length);
 8 
 9         for (Entry<K,V> e = tab[i]; e != null; e = e.next) {
10             if (h == e.hash && eq(k, e.get())) {
11                 V oldValue = e.value;
12                 if (value != oldValue)
13                     e.value = value;
14                 return oldValue;
15             }
16         }
17 
18         modCount++;
19         Entry<K,V> e = tab[i];
20         tab[i] = new Entry<>(k, value, queue, h, e);// queue被各Entry共享
21         if (++size >= threshold)
22             resize(tab.length * 2);
23         return null;
24     }
WeakHashMap put

往WeakHashMap添加元素时会调用Entry的构造方法,也就是会创建一个WeakReference对象,这个对象引用的是WeakHashMap刚加入的Key,而所有的WeakReference对象关联在同一个ReferenceQueue上,因此通过该队列可以知道所有被回收的key对象对应的Reference对象,即Entry对象。那么,queue里面的元素在WeakHashMap里面是在哪里被拿出去做了什么操作?关键在于WeakHashMap#expungeStaleEntries方法,相关源码:

 1 private void expungeStaleEntries() {
 2     //不断地从ReferenceQueue中取出,那些只有被WeakReference对象引用的对象的Reference
 3     for (Object x; (x = queue.poll()) != null; ) {
 4         synchronized (queue) {
 5             //转为 entry 
 6             Entry<K,V> e = (Entry<K,V>) x;
 7             //计算其对应的桶的下标
 8             int i = indexFor(e.hash, table.length);
 9             //取出桶中元素
10             Entry<K,V> prev = table[i];
11             Entry<K,V> p = prev;
12             //桶中对应位置有元素,遍历桶链表所有元素
13             while (p != null) {
14                 Entry<K,V> next = p.next;
15                 //如果当前元素(也就是entry)与queue取出的一致,将entry从链表中去除 
16                 if (p == e) {
17                     if (prev == e)
18                         table[i] = next;
19                     else
20                         prev.next = next;
21                     //清空entry对应的value
22                     e.value = null; // Help GC
23                     size--;
24                     break;
25                 }
26                 prev = p;
27                 p = next;
28             }
29         }
30     }
31 }
WeakHashMap expungeStaleEntries

虽然key对象被GC自动回收了,但与key相关的value、entry却还在,GC不会自动回收,因此如果不在代码中删除相应的这些数据就会导致memory leak。如何删除?从前面的expungeStaleEntries方法可以看出会循环从队列取entry、根据entry定位到桶、然后删除该桶中的链表上的该entry及value(值得注意的是这里index是根据entry计算的因为key已是null,而HashMap中是根据key计算的)。

expungeStaleEntries方法会被 getTable、size、resize 方法调用,而 getTable 又被 get、containsKey、put、remove、containsValue、replaceAll 等方法调用,最终看来,只要操作了WeakHashMap就会调用expungeStaleEntries方法。故只要没在被该Map以外的地方用到(强引用)的key,对应的key、entry、value就会被从该Map清除。

通过以上分析可知,WeakHashMap适合用在内存容量有限的场景,因为它能够自动对不再需要使用(强引用)的数据从Map中删除

注:key不再被强引用后会被GC自动回收(回收多少个、回收哪些key都是不固定的,由GC决定),key对应的entry、value是用户代码处理之后才自动被GC回收的。

 

4.5.3 ThreadLocalMap解决MemoryLeak

ThreadLocalMap 在未采用WeakReference时,若某个key对象被回收了,则同样会存在该key对应的Entry、value没有回收的问题,即Memory Leak。

解决方法与WeakHashMap大同小异,Entry也继承了WeakReference、key为referent。区别在于:

Entry创建时不再提供queue参数;

1         static class Entry extends WeakReference<ThreadLocal<?>> {
2             /** The value associated with this ThreadLocal. */
3             Object value;
4 
5             Entry(ThreadLocal<?> k, Object v) {
6                 super(k);
7                 value = v;
8             }
9         }
ThreadLocalMap Entry
 1         private void set(ThreadLocal<?> key, Object value) {
 2 
 3             // We don't use a fast path as with get() because it is at
 4             // least as common to use set() to create new entries as
 5             // it is to replace existing ones, in which case, a fast
 6             // path would fail more often than not.
 7 
 8             Entry[] tab = table;
 9             int len = tab.length;
10             int i = key.threadLocalHashCode & (len-1);
11 
12             for (Entry e = tab[i];
13                  e != null;
14                  e = tab[i = nextIndex(i, len)]) {
15                 ThreadLocal<?> k = e.get();
16 
17                 if (k == key) {
18                     e.value = value;
19                     return;
20                 }
21 
22                 if (k == null) {
23                     replaceStaleEntry(key, value, i);
24                     return;
25                 }
26             }
27 
28             tab[i] = new Entry(key, value);
29             int sz = ++size;
30             if (!cleanSomeSlots(i, sz) && sz >= threshold)
31                 rehash();
32         }
ThreadLocalMap#set

解决散列冲突用的是线性探测再散列,因此在内部存储上只有一个大数组而非桶链法;

只有在set时才会触发 expungeStaleEntries,且清除不用的对象对应的Reference对象时不是从queue取,而是遍历整个桶的元素,即Entry,判断Entry的key对应的对象是否已被回收;

 1         /**
 2          * Expunge all stale entries in the table.
 3          */
 4         private void expungeStaleEntries() {
 5             Entry[] tab = table;
 6             int len = tab.length;
 7             for (int j = 0; j < len; j++) {
 8                 Entry e = tab[j];
 9                 if (e != null && e.get() == null)
10                     expungeStaleEntry(j);
11             }
12         }
13 
14 
15        private int expungeStaleEntry(int staleSlot) {
16             Entry[] tab = table;
17             int len = tab.length;
18 
19             // expunge entry at staleSlot
20             tab[staleSlot].value = null;
21             tab[staleSlot] = null;
22             size--;
23 
24             // Rehash until we encounter null
25             Entry e;
26             int i;
27             for (i = nextIndex(staleSlot, len);
28                  (e = tab[i]) != null;
29                  i = nextIndex(i, len)) {
30                 ThreadLocal<?> k = e.get();
31                 if (k == null) {
32                     e.value = null;
33                     tab[i] = null;
34                     size--;
35                 } else {
36                     int h = k.threadLocalHashCode & (len - 1);
37                     if (h != i) {
38                         tab[i] = null;
39 
40                         // Unlike Knuth 6.4 Algorithm R, we must scan until
41                         // null because multiple entries could have been stale.
42                         while (tab[h] != null)
43                             h = nextIndex(h, len);
44                         tab[h] = e;
45                     }
46                 }
47             }
48             return i;
49         }
ThreadLocalMap expungeStaleEntries 

关于ThreadLocalMap的更多内容,可参阅 Java ThreadLocal - MarchOn 

 

 

题外话:

在探索上述原理的过程中发现  WeakHashMap、ThreadLocalMap 在计算hash、解决散列冲突方面与HashMap的区别:WeakHashMap根据entry计算hash、ThreadLocalMap用线性探测再散列解决hash冲突;在感知对象回收做自定义处理操作时,前者通过队列知道回收对象、后者通过全数组扫描知道回收对象。

计算hash:(table length 都要求满足2^n)

WeakHashMap(根据entry计算): h=entry.hash; length=table.length; return h & (length-1); 。由于queue里元素的key始终为null故根据entry计算。

ThreadLocalMap(根据key计算): return threadLocal.threadLocalHashCode & (len-1); 

HashMap(根据key计算): (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16) 

解决散列冲突:

WeakHashMap:桶链法

ThreadLocalMap:线性探测再散列。这是由使用场景决定的:该Map的key为ThreadLocal对象,在一个程序中并不会有很多个这种对象(撑死了上百?)。

HashMap:桶链法

 

 

5 参考资料

Java Reference原理分析

深入理解JDK中的Reference原理和源码实现

 

posted @ 2021-03-31 18:28  March On  阅读(647)  评论(0编辑  收藏  举报
top last
Welcome user from
(since 2020.6.1)