更多請移步: 我的博客
JDK源碼 Java的四種Reference
之前探討過一次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回收前,會將那些已經向引用隊列註冊的新清除的軟引用加入隊列。
public class ClassSoft {
public static class Referred {
/**
* 不是必須實現,和Strong不同。
* 實現該方法是爲了追蹤GC,
* 實現後也會被當作Finalizer
* @throws Throwable
*/
@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);
//此處異常可以說明,在被放入隊列之前referent已經被JVM置爲null
System.out.println("gc will collect: " + result.getClass() + "@" + result.hashCode());
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("Object for SoftReference is " + obj.get());
}
}
}
//如果我們使用了自定義的註冊隊列,一定要啓動一個線程來處理該隊列
//JVM只負責像隊列中放入對象,不負責清理
static ReferenceQueue<Referred> softQueue = new ReferenceQueue<>();
/**
* JVM配置
* -Xms4m -Xmx4m
* -XX:+PrintGCDetails -Xloggc:/Users/childe/logs/gc-f.log
* 務必加上該參數,以確定collect方法後GC被執行
* @param args
* @throws InterruptedException
*/
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");
}
}
程序輸出如下:
創建軟引用
切斷強引用
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的時候會進行回收。
/**
*
* Created by childe on 2017/3/31.
*/
public class ClassWeak {
public static class Referred {
/**
* 不是必須實現,和Strong不同。
* 實現該方法是爲了追蹤GC
* 實現後也會被當作Finalizer
* @throws Throwable
*/
@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);
}
/**
* JVM配置
* -XX:+PrintGCDetails -Xloggc:/Users/childe/logs/gc-f.log
* 務必加上該參數,以確定collect方法後GC被執行
* @param args
* @throws InterruptedException
*/
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");
}
}
程序輸出如下:
創建一個弱引用
切斷強引用
GC之前,弱引用值:I am weak
Referred對象被垃圾收集
Done
PhantomReference(虛引用)
如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。這個特性,決定了他的get方法每次調用均會返回null。
/**
* Created by childe on 2017/3/31.
*/
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) {
//因爲虛引用的指示對象總是不可到達的,所以此方法總是返回 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<>();
/**
* -Xms4m -Xmx4m
* @param args
* @throws InterruptedException
*/
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");
}
}
輸出如下:
創建一個軟引用
切斷強引用
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的這個特性來維護先進後出單向隊列(類似棧)。
public abstract class Reference<T> {
//......
//引用有4中概念上的狀態:Active、Pending、 Enqueued 、Inactive
//引用的初始態爲Active或者Pending,它的生命後期爲:(Active || Pending)-> Enqueued -> Inactive
private T referent; /* Treated specially by GC 由GC專門處理*/
ReferenceQueue<? super T> queue; /* Reference 關聯的引用隊列 */
Reference next; /* 指向下一個引用 */
//......
}
public class ReferenceQueue<T> {
//......
//如果我們構造Reference時,未傳入自定義隊列,默認使用此隊列。
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) { /* Called only by Reference class 只會對Reference類調用該方法 */
synchronized (r) {
//以入隊的引用不多次入隊
if (r.queue == ENQUEUED) return false;
synchronized (lock) {
//修改引用入隊狀態爲Enqueued
r.queue = ENQUEUED;
//插入對頭
r.next = (head == null) ? r : head;
head = r;
queueLength++;
if (r instanceof FinalReference) {
sun.misc.VM.addFinalRefCount(1);
}
//通知等待在鎖上的線程ReferenceQueue.remove()
lock.notifyAll();
return true;
}
}
}
private Reference<? extends T> reallyPoll() { /* Must hold lock 必須在持有lock鎖的情況下執行,lock由其外層方法獲取 */
if (head != null) {
//獲取隊頭
Reference<? extends T> r = head;
head = (r.next == r) ? null : r.next;
//將關聯的隊列置爲NULL,此時r的狀態爲Inactive,處於此狀態的引用不會再發生變化,等待被回收。
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。