PhantomReference虛引用 在分析堆外記憶體回收之前,先瞭解下PhantomReference虛引用。 PhantomReference需要與ReferenceQueue引用隊列結合使用,在GC進行垃圾回收的時候,如果發現一個對象只有虛引用在引用它,則認為該對象需要被回收,會將引用該對象的 ...
PhantomReference虛引用
在分析堆外記憶體回收之前,先瞭解下PhantomReference
虛引用。
PhantomReference
需要與ReferenceQueue
引用隊列結合使用,在GC進行垃圾回收的時候,如果發現一個對象只有虛引用在引用它,則認為該對象需要被回收,會將引用該對象的虛引用加入到與其關聯的ReferenceQueue
隊列中,開發者可以通過ReferenceQueue
獲取需要被回收的對象,然後做一些清理操作,從隊列中獲取過的元素會從隊列中清除,之後GC就可以對該對象進行回收。
虛引用提供了一種追蹤對象垃圾回收狀態的機制,讓開發者知道哪些對象準備進行回收,在回收之前開發者可以進行一些清理工作,之後GC就可以將對象進行真正的回收。
來看一個虛引用的使用例子:
- 創建一個
ReferenceQueue
隊列queue,用於跟蹤對象的回收; - 創建一個obj對象,通過new創建的是強引用,只要強引用存在,對象就不會被回收;
- 創建一個虛引用
PhantomReference
,將obj對象和ReferenceQueue
隊列傳入,此時phantomReference裡面引用了obj對象,並關聯著引用隊列queue; - 同樣的方式創建另一個obj1對象和虛引用對象phantomReference1;
- 將obj置為NULL,此時強引用關係失效;
- 調用
System.gc()
進行垃圾回收; - 由於obj的強引用關係失效,所以GC認為該對象需要被回收,會將引用該對象的虛引用phantomReference對象放入到與其關聯的引用隊列queue中;
- 通過
poll
從引用隊列queue中獲取對象,可以發現會獲取到phantomReference對象,poll
獲取之後會將對象從引用隊列中刪除,之後會被垃圾回收器回收; - obj1的強引用關係還在,所以從queue中並不會獲取到;
public static void main(String[] args) {
// 創建引用隊列
ReferenceQueue<Object> queue = new ReferenceQueue<Object>();
// 創建obj對象
Object obj = new Object();
// 創建虛引用,虛引用引用了obj對象,並與queue進行關聯
PhantomReference<Object> phantomReference = new PhantomReference<Object>(obj, queue);
// 創建obj1對象
Object obj1 = new Object();
PhantomReference<Object> phantomReference1 = new PhantomReference<Object>(obj1, queue);
// 將obj置為NULL,強引用關係失效
obj = null;
// 垃圾回收
System.gc();
// 從引用隊列獲取對象
Object o = queue.poll();
if (null != o) {
System.out.println(o.toString());
}
}
輸出結果:
java.lang.ref.PhantomReference@277c0f21
Reference實例的幾種狀態
Active:初始狀態,創建一個Reference類型的實例之後處於Active狀態,以上面虛引用為例,通過new創建一個PhantomReference
虛引用對象之後,虛引用對象就處於Active狀態。
Pending:當GC檢測到對象的可達性發生變化時,會根據是否關聯了引用隊列來決定是否將狀態更改為Pending或者Inactive,虛引用必須與引用隊列結合使用,所以對於虛引用來說,如果它實際引用的對象需要被回收,垃圾回收器會將這個虛引用對象加入到一個Pending列表中,此時處於Pending狀態。
同樣以上面的的虛引用為例,因為obj的強引用關係失效,GC就會把引用它的虛引用對象放入到pending列表中。
Enqueued:表示引用對象被加入到了引用隊列,Reference有一個後臺線程去檢測是否有處於Pending狀態的引用對象,如果有會將引用對象加入到與其關聯的引用隊列中,此時由Pending轉為Enqueued狀態表示對象已加入到引用隊列中。
Inactive:通過引用隊列的poll
方法可以從引用隊列中獲取引用對象,同時引用對象會從隊列中移除,此時引用對象處於Inactive狀態,之後會被GC回收。
DirectByteBuffer堆外記憶體回收
在DirectByteBuffer
的構造函數中,在申請記憶體之前,先調用了Bits
的reserveMemory
方法回收記憶體,申請記憶體之後,調用Cleaner
的create
方法創建了一個Cleaner
對象,並傳入了當前對象(DirectByteBuffer)和一個Deallocator
類型的對象:
class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer {
private final Cleaner cleaner;
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
// 清理記憶體
Bits.reserveMemory(size, cap);
long base = 0;
try {
// 分配記憶體
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
// 創建Cleader,傳入了當前對象和Deallocator
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
}
Cleaner
從名字上可以看出與清理有關,Bits
的reserveMemory
方法底層也是通過Cleaner
來進行清理,所以Cleaner是重點關註的類。
Deallocator
是DirectByteBuffer
的一個內部類,並且實現了Runnable介面,在run方法中可以看到對記憶體進行了釋放,接下來就去看下在哪裡觸發Deallocator
任務的執行:
class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer {
private static class Deallocator implements Runnable {
// ...
private Deallocator(long address, long size, int capacity) {
assert (address != 0);
this.address = address; // 設置記憶體地址
this.size = size;
this.capacity = capacity;
}
public void run() {
if (address == 0) {
// Paranoia
return;
}
// 釋放記憶體
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
}
}
Cleaner
Cleaner
繼承了PhantomReference
,PhantomReference
是Reference
的子類,所以Cleaner
是一個虛引用對象。
創建Cleaner
虛引用需要與引用隊列結合使用,所以在Cleaner中可以看到有一個ReferenceQueue
,它是一個靜態的變數,所以創建的所有Cleaner對象都會共同使用這個引用隊列。
在創建Cleaner的create
方法中,處理邏輯如下:
- 通過構造函數創建了一個Cleaner對象,構造函數中的referent參數為
DirectByteBuffer
,thunk參數為Deallocator
對象,在構造函數中又調用了父類的構造函數完成實例化; - 調用add方法將創建的Cleaner對象加入到鏈表中,添加到鏈表的時候使用的是頭插法,新加入的節點放在鏈表的頭部,first成員變數是一個靜態變數,它指向鏈表的頭結點,創建的Cleaner都會加入到這個鏈表中;
創建後的Cleaner對象處於Active狀態。
public class Cleaner extends PhantomReference<Object>{
// ReferenceQueue隊列
private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>();
// 靜態變數,鏈表的頭結點,創建的Cleaner都會加入到這個鏈表中
static private Cleaner first = null;
// thunk
private final Runnable thunk;
public static Cleaner create(Object ob, Runnable thunk) {
if (thunk == null)
return null;
// 創建一個Cleaner並加入鏈表
return add(new Cleaner(ob, thunk));
}
private Cleaner(Object referent, Runnable thunk) {
super(referent, dummyQueue); // 調用父類構造函數,傳入引用對象和引用隊列
this.thunk = thunk; // thunk指向傳入的Deallocator
}
private static synchronized Cleaner add(Cleaner cl) {
// 如果頭結點不為空
if (first != null) {
// 將新加入的節點作為頭結點
cl.next = first;
first.prev = cl;
}
first = cl;
return cl;
}
}
Cleaner調用父類構造函數時,最終會進入到父類Reference
中的構造函數中:
referent:指向實際的引用對象,上面創建的是DirectByteBuffer
,所以這裡指向的是DirectByteBuffer
。
queue:引用隊列,指向Cleaner
中的引用隊列dummyQueue
。
public class PhantomReference<T> extends Reference<T> {
// ...
public PhantomReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q); // 調用父類構造函數
}
}
public abstract class Reference<T> {
/* 引用對象 */
private T referent;
// 引用隊列
volatile ReferenceQueue<? super T> queue;
Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
// 設置引用隊列
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
}
啟動ReferenceHandler線程
Reference
中有一個靜態方法,裡面創建了一個ReferenceHandler
並設置為守護線程,然後啟動了該線程,並創建了JavaLangRefAccess
對象設置到SharedSecrets
中:
public abstract class Reference<T> {
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
// 創建ReferenceHandler
Thread handler = new ReferenceHandler(tg, "Reference Handler");
// 設置優先順序為最高
handler.setPriority(Thread.MAX_PRIORITY);
handler.setDaemon(true);
handler.start();
// 這裡設置了JavaLangRefAccess
SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
@Override
public boolean tryHandlePendingReference() {
// 調用了tryHandlePending
return tryHandlePending(false);
}
});
}
}
ReferenceHandler
是Reference
的內部類,繼承了Thread
,在run方法中開啟了一個迴圈,不斷的執行tryHandlePending
方法,處理Reference中
的pending
列表:
public abstract class Reference<T> {
private static class ReferenceHandler extends Thread {
// ...
ReferenceHandler(ThreadGroup g, String name) {
super(g, name);
}
public void run() {
while (true) {
// 處理pending列表
tryHandlePending(true);
}
}
}
}
Cleaner會啟動一個優先順序最高的守護線程,不斷調用tryHandlePending來檢測是否有需要回收的引用對象(還未進行真正的回收),然後進行處理。
處理pending列表
垃圾回收器會將要回收的引用對象放在Reference
的pending
變數中,從數據類型上可以看出pending
只是一個Reference
類型的對象,並不是一個list,如果有多個需要回收的對象,如何將它們全部放入pending
對象中?
可以把pengding看做是一個鏈表的頭結點,假如有引用對象被判定需要回收,如果pengding為空直接放入即可,如果不為空,將使用頭插法將新的對象加入到鏈表中,也就是將新對象的discovered指向pending對象,然後將pending指向當前要回收的這個對象,這樣就形成了一個鏈表,pending指向鏈表的頭結點。
在pending鏈表中的引用對象處於pending狀態。
接下來看tryHandlePending
方法的處理邏輯:
-
如果pending不為空,表示有需要回收的對象,此時將pengding指向的對象放在臨時變數
r
中,並判斷是否是Cleaner類型,如果是將其強制轉為Cleaner
,記錄在臨時變數c
中,接著更新pending的值為r的discovered,因為discovered中記錄了下一個需要被回收的對象,pengding需要指向下一個需要被回收的對象;pending如果為NULL,會進入到else的處理邏輯,返回值為參數傳入的waitForNotify的值。
-
判斷
Cleaner
對象是否為空,如果不為空,調用Cleaner的clean方法進行清理; -
獲取引用對象關聯的引用隊列,然後調用
enqueue
方法將引用對象加入到引用隊列中; -
返回true;
public abstract class Reference<T> {
// 指向pending列表中的下一個節點
transient private Reference<T> discovered;
// 靜態變數pending列表,可以看做是一個鏈表,pending指向鏈表的頭結點
private static Reference<Object> pending = null;
static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
Cleaner c;
try {
synchronized (lock) {
// 如果pending不為空
if (pending != null) {
// 獲取pending執行的對象
r = pending;
// 如果是Cleaner類型
c = r instanceof Cleaner ? (Cleaner) r : null;
// 將pending指向下一個節點
pending = r.discovered;
// 將discovered置為空
r.discovered = null;
} else {
// 等待
if (waitForNotify) {
lock.wait();
}
return waitForNotify;
}
}
} catch (OutOfMemoryError x) {
Thread.yield();
// retry
return true;
} catch (InterruptedException x) {
// retry
return true;
}
if (c != null) {
// 調用clean方法進行清理
c.clean();
return true;
}
// 獲取引用隊列
ReferenceQueue<? super Object> q = r.queue;
// 如果隊列不為空,將對象加入到引用隊列中
if (q != ReferenceQueue.NULL) q.enqueue(r);
// 返回true
return true;
}
}
釋放記憶體
在Cleaner
的clean方法中,可以看到,調用了thunk的run方法,前面內容可知,thunk指向的是Deallocator對象,所以會執行Deallocator的run方法,Deallocator的run方法前面也已經看過,裡面會對DirectByteBuffer的堆外記憶體進行釋放:
public class Cleaner extends PhantomReference<Object> {
public void clean() {
if (!remove(this))
return;
try {
// 調用run方法
thunk.run();
} catch (final Throwable x) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
if (System.err != null)
new Error("Cleaner terminated abnormally", x)
.printStackTrace();
System.exit(1);
return null;
}});
}
}
}
總結
Cleaner是一個虛引用,它實際引用的對象DirectByteBuffer如果被GC判定為需要回收,會將引用該對象的Cleaner加入到pending列表,ReferenceHandler線程會不斷檢測pending是否為空,如果不為空,就對其進行處理:
- 如果對象類型為Cleaner,就調用Cleaner的clean方法進行清理,Cleaner的clean方法又會調用Deallocator的run方法,裡面調用了freeMemory方法對DirectByteBuffer分配的堆外記憶體進行釋放;
- 將Cleaner對象加入到與其關聯的引用隊列中;
引用隊列
ReferenceQueue
名字聽起來是一個隊列,實際使用了一個鏈表,使用頭插法將加入的節點串起來,ReferenceQueue
中的head
變數指向鏈表的頭節點,每個節點是一個Reference類型的對象:
public class ReferenceQueue<T> {
// head為鏈表頭節點
private volatile Reference<? extends T> head = null;
}
Reference
中除了discovered變數之外,還有一個next變數,discovered指向的是處於pending狀態時pending列表中的下一個元素,next變數指向的是處於Enqueued狀態時,引用隊列中的下一個元素:
public abstract class Reference<T> {
/* When active: 處於active狀態時為NULL
* pending: this
* Enqueued: Enqueued狀態時,指向引用隊列中的下一個元素
* Inactive: this
*/
@SuppressWarnings("rawtypes")
Reference next;
/* When active: active狀態時,指向GC維護的一個discovered鏈表中的下一個元素
* pending: pending狀態時,指向pending列表中的下一個元素
* otherwise: 其他情況為NULL
*/
transient private Reference<T> discovered; /* used by VM */
}
enqueue入隊
進入引用隊列中的引用對象處於enqueue狀態。
enqueue
的處理邏輯如下:
- 判斷要加入的對象關聯的引用隊列,對隊列進行判斷,如果隊列為空或者隊列等於
ReferenceQueue
中的空隊列ENQUEUED
,表示該對象之前已經加入過隊列,不能重覆操作,返回false,如果未加入過繼續下一步; - 將對象所關聯的引用隊列置為
ENQUEUED
,它是一個空隊列,表示節點已經加入到隊列中; - 判斷頭節點是否為空,如果為空,表示鏈表還沒有節點,將當前對象的next指向自己,如果頭結點不為空,將當前對象的next指向頭結點,然後更新頭結點的值為當前對象(頭插法插入鏈表);
- 增加隊列的長度,也就是鏈表的長度;
public class ReferenceQueue<T> {
// 空隊列
static ReferenceQueue<Object> ENQUEUED = new Null<>();
// 入隊,將節點加入引用隊列,隊列實際上是一個鏈表
boolean enqueue(Reference<? extends T> r) {
synchronized (lock) {
// 獲取關聯的引用隊列
ReferenceQueue<?> queue = r.queue;
// 如果為空或者已經添加到過隊列
if ((queue == NULL) || (queue == ENQUEUED)) {
return false;
}
assert queue == this;
// 將引用隊列置為一個空隊列,表示該節點已經入隊
r.queue = ENQUEUED;
// 如果頭結點為空將下一個節點置為自己,否則將next置為鏈表的頭結點,可以看出同樣使用的是頭插法將節點插入鏈表
r.next = (head == null) ? r : head;
// 更新頭結點為當前節點
head = r;
// 增加長度
queueLength++;
if (r instanceof FinalReference) {
sun.misc.VM.addFinalRefCount(1);
}
lock.notifyAll();
return true;
}
}
}
poll出隊
在調用poll
方法從引用隊列中獲取一個元素並出隊的時候,首先對head頭結點進行判空,如果為空表示引用隊列中沒有數據,返回NULL,否則調用reallyPoll
從引用隊列中獲取元素。
出隊的處理邏輯如下:
-
獲取鏈表中的第一個節點也就是頭結點,如果不為空進行下一步;
-
如果頭節點的下一個節點是自己,表示鏈表只有一個節點,頭結點出隊之後鏈表為空,所以將頭結點的值更新為NULL;
如果頭節點的下一個節點不是自己,表示鏈表中還有其他節點,更新head頭節點的值為下一個節點,也就是next指向的對象;
-
將需要出隊的節點的引用隊列置為NULL,next節點置為自己,表示節點已從隊列中刪除;
-
引用隊列的長度減一;
-
返回要出隊的節點;
從出隊的邏輯中可以看出,引用隊列中的對象是後進先出的,poll出隊之後的引用對象處於Inactive狀態,表示可以被GC回收掉。
public class ReferenceQueue<T> {
/**
* 從引用隊列中獲取一個節點,進行出隊操作
*/
public Reference<? extends T> poll() {
// 如果頭結點為空,表示沒有數據
if (head == null)
return null;
synchronized (lock) {
return reallyPoll();
}
}
@SuppressWarnings("unchecked")
private Reference<? extends T> reallyPoll() { 、 /* Must hold lock */
// 獲取頭結點
Reference<? extends T> r = head;
if (r != null) {
// 如果頭結點的下一個節點是自己,表示鏈表只有一個節點,head置為null,否則head值為r的下一個節點,也就是next指向的對象
head = (r.next == r) ?
null :
r.next;
// 將引用隊列置為NULL
r.queue = NULL;
// 下一個節點置為自己
r.next = r;
// 長度減一
queueLength--;
if (r instanceof FinalReference) {
sun.misc.VM.addFinalRefCount(-1);
}
// 返回鏈表中的第一個節點
return r;
}
return null;
}
}
reserveMemory記憶體清理
最開始在DirectByteBuffer的構造函數中看到申請記憶體之前會調用Bits的reserveMemory
方法,如果沒有足夠的記憶體,它會從SharedSecrets
獲取JavaLangRefAccess
對象進行一些處理,由前面的內容可知,Reference
中的靜態方法啟動ReferenceHandler
之後,創建了JavaLangRefAccess
並設置到SharedSecrets
中,所以這裡調用JavaLangRefAccess
的tryHandlePendingReference
實際上依舊調用的是Reference
中的tryHandlePending
方法。
在調用Reference
中的tryHandlePending
方法處理需要回收的對象之後,調用tryReserveMemory
方法判斷是否有足夠的記憶體,如果記憶體依舊不夠,會調用` System.gc()觸發垃圾回收,然後開啟一個迴圈,處理邏輯如下:
-
判斷記憶體是否充足,如果充足直接返回;
-
判斷睡眠次數是否小於限定的最大值,如果小於繼續下一步,否則終止迴圈;
-
調用tryHandlePendingReference處理penging列表中的引用對象,前面在處理pending列表的邏輯中可以知道,如果pending列表不為空,會返回true,tryHandlePendingReference也會返回true,此時意味著清理了一部分對象,所以重新進入到第1步進行檢查;
如果pending列表為空,會返回參數中傳入的waitForNotify的值,從JavaLangRefAccess的tryHandlePendingReference中可以看出這裡傳入的是false,所以會進行如下處理:
- 通過
Thread.sleep(sleepTime)
讓當前線程睡眠一段時間,這樣可以避免reserveMemory方法一直在占用資源; - 對睡眠次數加1;
- 通過
-
如果以上步驟處理之後還沒有足夠的空間會拋出拋出OutOfMemoryError異常;
reserveMemory方法的作用是保證在申請記憶體之前有足夠的記憶體,如果沒有足夠的記憶體會進行清理,達到指定清理次數之後依舊沒有足夠的記憶體空間,將拋出OutOfMemoryError異常。
class Bits {
static void reserveMemory(long size, int cap) {
if (!memoryLimitSet && VM.isBooted()) {
maxMemory = VM.maxDirectMemory();
memoryLimitSet = true;
}
// 是否有足夠記憶體
if (tryReserveMemory(size, cap)) {
return;
}
// 獲取JavaLangRefAccess
final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
// 調用tryHandlePendingReference
while (jlra.tryHandlePendingReference()) {
// 判斷是否有足夠的記憶體
if (tryReserveMemory(size, cap)) {
return;
}
}
// 調用gc進行垃圾回收
System.gc();
boolean interrupted = false;
try {
long sleepTime = 1;
int sleeps = 0;
// 開啟迴圈
while (true) {
// 是否有足夠記憶體
if (tryReserveMemory(size, cap)) {
return;
}
// 如果次數小於最大限定次數,終止
if (sleeps >= MAX_SLEEPS) {
break;
}
// 再次處理penging列表中的對象
if (!jlra.tryHandlePendingReference()) {
try {
// 睡眠一段時間
Thread.sleep(sleepTime);
sleepTime <<= 1;
sleeps++; // 睡眠次數增加1
} catch (InterruptedException e) {
interrupted = true;
}
}
}
// 拋出OutOfMemoryError異常
throw new OutOfMemoryError("Direct buffer memory");
} finally {
if (interrupted) {
// don't swallow interrupts
Thread.currentThread().interrupt();
}
}
}
}
public abstract class Reference<T> {
static {
// ...
// 這裡設置了JavaLangRefAccess
SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
@Override
public boolean tryHandlePendingReference() {
// 調用tryHandlePending,這裡waitForNotify參數傳入的是false
return tryHandlePending(false);
}
});
}
}
參考
Java 源碼剖析——徹底搞懂 Reference 和 ReferenceQueue