一文帶你理解jvm之強軟弱虛引用
強軟弱虛引用
在java中,除了基本資料型別的變數外,其他所有的變數都是引用型別,指向堆上各種不同的物件。
在jvm中,除了我們常用的強引用外,還有軟引用、弱引用、虛引用,這四種引用型別的生命週期與jvm的垃圾回收過程息息相關。
那麼這四種引用型別有什麼區別?具體使用場景是什麼?
所有引用型別,都是抽象類java.lang.ref.Reference的子類,這個類的主要方法為get()方法:
public T get() {
return this.referent;
}
除了虛引用(因為get永遠返回null),如果物件還沒有被銷燬,都可以通過get方法獲取原有物件。這意味著,利用軟引用和弱引用,我們可以將訪問到的物件,重新指向強引用,也就是人為的改變了物件的可達性狀態!
強引用
強引用(Strong references)就是直接new一個普通物件,表示一種比較強的引用關係,只要還有強引用物件指向一個物件,那麼表示這個物件還活著,垃圾收集器寧可丟擲OOM異常,也不會回收這個物件。
例如下面的引用u就是一個強引用。
public class StrongReferenceDemo {
public static void main(String[] args) throws IOException {
User u = new User();
System.out.println(u);
u = null;
System.gc();
System.in.read();
}
}
public class User {
@Override
protected void finalize() throws Throwable {
System.out.println("call User finalize() method");
}
}
上面的User物件重寫了父類Object的finalize(),在GC準備釋放物件所佔用的記憶體空間之前,它將首先呼叫finalize()方法。
在Java中,由於GC的自動回收機制,因而並不能保證finalize方法會被及時地執行(垃圾物件的回收時機具有不確定性),也不能保證它們會被執行(程式由始至終都未觸發垃圾回收),所以finalize不推薦使用,這裡只是為了演示垃圾回收的過程。
另外finalize()最多隻會被呼叫一次,也就是隻能利用finalize()為物件續命一次。
軟引用
軟引用用於儲存一些可有可無的東西,例如快取,當系統記憶體充足時,這些物件不會被回收,當系統記憶體不足時也是GC時才會回收這些物件,如果回收完這些物件後記憶體還是不足,就會丟擲OOM異常。
// vm args: -Xmx36m -XX:+PrintGCDetails
public class SoftReferenceDemo {
public static void main(String[] args) throws InterruptedException {
SoftReference<User> softReference = new SoftReference<>(new User()); // 軟引用
System.out.println(softReference.get());
System.gc();
TimeUnit.SECONDS.sleep(3); // wait gc thread run
System.out.println(softReference.get()); // User物件不會被回收
byte[] bytes = new byte[1024 * 1024 * 10]; // 分配一個大物件使得堆空間不足,軟引用物件會在OOM之前先被回收
System.out.println(softReference.get());
}
}
在上面的例子中,第一次發生gc時,User物件不會被回收,第二次發生gc時由於堆空間不足,會先回收軟引用的物件,回收完了還是空間不足,最後丟擲OOM異常。
弱引用
弱引用(WeakReference)並不能使物件豁免垃圾回收,僅僅是提供一種訪問在弱引用狀態下物件的途徑。只要發生gc,弱引用物件就會被回收。ThreadLocal中就使用了WeakReference來避免記憶體洩漏。
public class WeakReferenceDemo {
public static void main(String[] args) throws InterruptedException {
WeakReference<User> weakReference = new WeakReference<>(new User());
System.out.println(weakReference.get());
System.gc();
TimeUnit.SECONDS.sleep(3); // wait gc thread run
System.out.println(weakReference.get()); // null
}
}
上面的例子只要發生gc,User物件就會被垃圾收集器回收。
虛引用
虛引用必須和引用佇列(ReferenceQueue)聯合使用。如果一個物件僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。
虛引用主要用來跟蹤物件被垃圾回收的活動。當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就會在回收物件之前,把這個虛引用加入到與之關聯的引用佇列中。程式如果發現某個虛引用已經被加入到引用佇列,那麼就可以在所引用的物件的記憶體被回收之前採取必要的行動。
// vm args: -Xms4m -XX:+PrintGC
public class PhantomReferenceDemo {
public static void main(String[] args) throws IOException, InterruptedException {
ReferenceQueue<User> referenceQueue = new ReferenceQueue<>(); // 引用佇列
PhantomReference<User> phantomReference = new PhantomReference<>(new User(), referenceQueue); // 虛引用
System.out.println(phantomReference.get()); // null
new Thread(() -> {
while (true) {
Reference<? extends User> poll = referenceQueue.poll();
if (poll != null) {
System.out.println("--- 虛引用物件被jvm回收了 ---- " + poll);
System.out.println("--- 回收物件 ---- " + poll.get()); // null
}
}
}).start();
TimeUnit.SECONDS.sleep(1);
System.gc();
System.in.read();
}
private static class User {
private int[] bytes = new int[1024 * 1024 * 5];
@Override
protected void finalize() throws Throwable {
System.out.println("call User finalize() method");
}
}
}
實際上,虛引用的get()方法總是返回null。
基於虛引用,有一個更加優雅的實現方式,那就是Cleaner,可以用來替代Object類的finalizer方法,在DirectByteBuffer中用來回收堆外記憶體。