Java 堆外內(nèi)存回收原理
簡(jiǎn)書(shū)滌生俺附。
轉(zhuǎn)載請(qǐng)注明原創(chuàng)出處,謝謝溪掀!
如果讀完覺(jué)得有收獲的話(huà),歡迎點(diǎn)贊加關(guān)注揪胃。
DirectByteBuffer 簡(jiǎn)介
DirectByteBuffer 這個(gè)類(lèi)是 JDK 提供使用堆外內(nèi)存的一種途徑蛮浑,當(dāng)然常見(jiàn)的業(yè)務(wù)開(kāi)發(fā)一般不會(huì)接觸到只嚣,即使涉及到也可能是框架(如 Netty沮稚、RPC 等)使用的册舞,對(duì)框架使用者來(lái)說(shuō)也是透明的。
堆外內(nèi)存的優(yōu)勢(shì)
堆外內(nèi)存優(yōu)勢(shì)在 IO 操作上调鲸,對(duì)于網(wǎng)絡(luò) IO盛杰,使用 Socket 發(fā)送數(shù)據(jù)時(shí),能夠節(jié)省堆內(nèi)存到堆外內(nèi)存的數(shù)據(jù)拷貝即供,所以性能更高∮谖ⅲ看過(guò) Netty 源碼的同學(xué)應(yīng)該了解逗嫡,Netty 使用堆外內(nèi)存池來(lái)實(shí)現(xiàn)零拷貝技術(shù)株依。對(duì)于磁盤(pán) IO 時(shí),也可以使用內(nèi)存映射恋腕,來(lái)提升性能抹锄。
另外,更重要的幾乎不用考慮堆內(nèi)存煩人的 GC 問(wèn)題荠藤。
堆外內(nèi)存的創(chuàng)建
我們直接來(lái)看代碼,首先向 Bits 類(lèi)申請(qǐng)額度哈肖,Bits 類(lèi)內(nèi)部維護(hù)著當(dāng)前已經(jīng)使用的堆外內(nèi)存值吻育,會(huì) check 當(dāng)前申請(qǐng)的大小與已經(jīng)使用的內(nèi)存大小是否超過(guò)總的堆外內(nèi)存大心党埂(默認(rèn)大小與堆內(nèi)存差不多扫沼,其實(shí)是有細(xì)微區(qū)別的庄吼,拿 CMS GC 來(lái)舉例,它的大小是新生代的最大值 - 一個(gè) survivor 的大小 + 老生代的最大值)总寻,可以使用 -XX:MaxDirectMemorySize 參數(shù)指定堆外內(nèi)存最大大小器罐。
//
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;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
如果 check 不通過(guò)渐行,會(huì)主動(dòng)執(zhí)行 System.gc()轰坊,然后 sleep 100 毫秒祟印,再進(jìn)行 check肴沫,如果內(nèi)存還是不足蕴忆,就拋出 OOM Error。
如果 check 通過(guò)套鹅,就會(huì)調(diào)用 unsafe.allocateMemory 真正分配內(nèi)存站蝠,返回內(nèi)存地址,然后再將內(nèi)存清 0卓鹿。題外話(huà),這個(gè) unsafe 命名看著是不是很?chē)樔艘魉铮@個(gè) unsafe 不是說(shuō)不安全澜倦,而是 JDK 內(nèi)部使用的類(lèi)杰妓,不推薦外部使用肥隆,所以叫 unsafe稚失,Netty 源碼內(nèi)部也有類(lèi)似命名。
由于申請(qǐng)內(nèi)存前可能會(huì)調(diào)用 System.gc()句各,所以謹(jǐn)慎設(shè)置 -XX:+DisableExplicitGC 這個(gè)選項(xiàng)吸占,這個(gè)參數(shù)作用是禁止代碼中顯示觸發(fā)的 Full GC。
堆外內(nèi)存的回收
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
看到這段代碼從成員的命名上就應(yīng)該知道矾屯,是用來(lái)回收堆外內(nèi)存的。確實(shí)初厚,但是它是如何工作的呢件蚕?接下來(lái)我們看看 Cleaner 類(lèi)。
public class Cleaner extends PhantomReference {
private static final ReferenceQueue dummyQueue = new ReferenceQueue();
private static Cleaner first = null;
private Cleaner next = null;
private Cleaner prev = null;
private final Runnable thunk;
private static synchronized Cleaner add(Cleaner var0) {
...
}
private static synchronized boolean remove(Cleaner var0) {
...
}
private Cleaner(Object var1, Runnable var2) {
super(var1, dummyQueue);
this.thunk = var2;
}
public static Cleaner create(Object var0, Runnable var1) {
return var1 == null?null:add(new Cleaner(var0, var1));
}
public void clean() {
if(remove(this)) {
try {
this.thunk.run();
} catch (final Throwable var2) {
AccessController.doPrivileged(new PrivilegedAction() {
public Void run() {
if(System.err != null) {
(new Error("Cleaner terminated abnormally", var2)).printStackTrace();
}
System.exit(1);
return null;
}
});
}
}
}
}
Cleaner 類(lèi)排作,內(nèi)部維護(hù)了一個(gè) Cleaner 對(duì)象的鏈表牵啦,通過(guò) create(Object, Runnable) 方法創(chuàng)建 cleaner 對(duì)象妄痪,調(diào)用自身的 add 方法哈雏,將其加入到鏈表中衫生。
更重要的是提供了 clean 方法,clean 方法首先將對(duì)象自身從鏈表中刪除罪针,保證只調(diào)用一次彭羹,然后執(zhí)行 this.thunk 的 run 方法,thunk 就是由創(chuàng)建時(shí)傳入的 Runnable 參數(shù)泪酱,也就是說(shuō) clean 只負(fù)責(zé)觸發(fā) Runnable 的 run 方法,至于 Runnable 做什么任務(wù)它不關(guān)心西篓。
那 DirectByteBuffer 傳進(jìn)來(lái)的 Runnable是什么呢愈腾?
private static class Deallocator
implements Runnable
{
private static Unsafe unsafe = Unsafe.getUnsafe();
private long address;
private long size;
private int capacity;
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);
}
}
Deallocator 類(lèi)的對(duì)象就是 DirectByteBuffer 中的 cleaner 傳進(jìn)來(lái)的 Runnable 參數(shù)類(lèi)岂津,我們直接看 run 方法 unsafe.freeMemory 釋放內(nèi)存,然后更新 Bits 里已使用的內(nèi)存數(shù)據(jù)吮成。
接下來(lái)我們關(guān)注各個(gè)環(huán)節(jié)是如何串起來(lái)的橱乱?這里主要講兩種回收方式:一種是自動(dòng)回收,一種是手動(dòng)回收粱甫。
如何自動(dòng)回收?
Java 是不用用戶(hù)去管理內(nèi)存的茶宵,所以 Java 對(duì)堆外內(nèi)存 默認(rèn)是自動(dòng)回收的危纫。
它是 由 GC 模塊負(fù)責(zé)的乌庶,在 GC 時(shí)會(huì)掃描 DirectByteBuffer 對(duì)象是否有有效引用指向該對(duì)象种蝶,如沒(méi)有瞒大,在回收 DirectByteBuffer 對(duì)象的同時(shí)且會(huì)回收其占用的堆外內(nèi)存螃征。但是 JVM 如何釋放其占用的堆外內(nèi)存呢透敌?如何跟 Cleaner 關(guān)聯(lián)起來(lái)呢踢械?
這得從 Cleaner 繼承了 PhantomReference(虛引用) 說(shuō)起。說(shuō)到 Reference魄藕,還有 SoftReference内列、WeakReference泼疑、FinalReference 他們作用各不相同,這里就不展開(kāi)說(shuō)了退渗。
簡(jiǎn)單介紹 PhantomReference,首先虛引用是不會(huì)影響 JVM 去回收其指向的對(duì)象蕴纳,當(dāng) GC 某個(gè)對(duì)象時(shí)会油,如果有此對(duì)象上還有虛引用對(duì)其引用古毛,會(huì)將 PhantomReference 對(duì)象插入 ReferenceQueue 隊(duì)列翻翩。
PhantomReference插入到哪個(gè)隊(duì)列呢稻薇?
看 PhantomReference 類(lèi)代碼嫂冻,其繼承自 Reference塞椎,Reference 對(duì)象有個(gè) ReferenceQueue 成員,這個(gè)也就是 PhantomReference 對(duì)象插入的 ReferenceQueue 隊(duì)列案狠,此成員如果不由外部傳入就是 ReferenceQueue.NULL服傍。如果需要通過(guò) queue 拿到 PhantomReference 對(duì)象,這個(gè) ReferenceQueue 對(duì)象還是必須由外部傳入骂铁。
private static final ReferenceQueue dummyQueue = new ReferenceQueue();
private Cleaner(Object var1, Runnable var2) {
super(var1, dummyQueue);
this.thunk = var2;
}
public class PhantomReference<T> extends Reference<T> {
Reference 類(lèi)內(nèi)部 static 靜態(tài)塊會(huì)啟動(dòng) ReferenceHandler 線(xiàn)程,線(xiàn)程優(yōu)先級(jí)很高拉庵,這個(gè)線(xiàn)程是用來(lái)處理 JVM 在 GC 過(guò)程中交接過(guò)來(lái)的 reference灿椅。想必經(jīng)常用 jstack 命令钞支,看線(xiàn)程堆棧的同學(xué)應(yīng)該見(jiàn)到過(guò)這個(gè)線(xiàn)程阱扬。
public abstract class Reference<T> {
private T referent; /* Treated specially by GC */
ReferenceQueue<? super T> queue;
Reference next;
transient private Reference<T> discovered; /* used by VM */
static private class Lock { };
private static Lock lock = new Lock();
private static Reference pending = null;
...
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread handler = new ReferenceHandler(tg, "Reference Handler");
/* If there were a special system-only priority greater than
* MAX_PRIORITY, it would be used here
*/
handler.setPriority(Thread.MAX_PRIORITY);
handler.setDaemon(true);
handler.start();
}
public T get() {
return this.referent;
}
public void clear() {
this.referent = null;
}
public boolean isEnqueued() {
synchronized (this) {
return (this.queue != ReferenceQueue.NULL) && (this.next != null);
}
}
public boolean enqueue() {
return this.queue.enqueue(this);
}
/* -- Constructors -- */
Reference(T referent) {
this(referent, null);
}
Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
}
我們來(lái)看看 ReferenceHandler 是如何處理的伸辟?
直接看 run 方法,首先是個(gè)死循環(huán)信夫,一直在那不停的干活窃蹋,synchronized 塊內(nèi)的這段主要是交接 JVM 扔過(guò)來(lái)的 reference(就是 pending)卡啰,再往下看警没,很明顯,調(diào)用了 cleaner 的 clean 方法杀迹。調(diào)完之后直接 continue 結(jié)束此次循環(huán)亡脸,這個(gè) reference 并沒(méi)有進(jìn)入 queue,也就是說(shuō) Cleaner 虛引用是不放入 ReferenceQueue树酪。
/* High-priority thread to enqueue pending References
*/
private static class ReferenceHandler extends Thread {
ReferenceHandler(ThreadGroup g, String name) {
super(g, name);
}
public void run() {
for (;;) {
Reference r;
synchronized (lock) {
if (pending != null) {
r = pending;
Reference rn = r.next;
pending = (rn == r) ? null : rn;
r.next = r;
} else {
try {
lock.wait();
} catch (InterruptedException x) { }
continue;
}
}
// Fast path for cleaners
if (r instanceof Cleaner) {
((Cleaner)r).clean();
continue;
}
ReferenceQueue q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
}
}
}
這塊有點(diǎn)想不通,既然不放入 ReferenceQueue续语,為什么 Cleaner 類(lèi)還是初始化了這個(gè) ReferenceQueue垂谢。
如何手動(dòng)回收疮茄?
手動(dòng)回收滥朱,就是由開(kāi)發(fā)手動(dòng)調(diào)用 DirectByteBuffer 的 cleaner 的 clean 方法來(lái)釋放空間力试。由于 cleaner 是 private 反問(wèn)權(quán)限徙邻,所以自然想到使用反射來(lái)實(shí)現(xiàn)畸裳。
public static void clean(final ByteBuffer byteBuffer) {
if (byteBuffer.isDirect()) {
Field cleanerField = byteBuffer.getClass().getDeclaredField("cleaner");
cleanerField.setAccessible(true);
Cleaner cleaner = (Cleaner) cleanerField.get(byteBuffer);
cleaner.clean();
}
}
還有另一種方法,DirectByteBuffer 實(shí)現(xiàn)了 DirectBuffer 接口躯畴,這個(gè)接口有 cleaner 方法可以獲取 cleaner 對(duì)象民鼓。
public static void clean(final ByteBuffer byteBuffer) {
if (byteBuffer.isDirect()) {
((DirectBuffer)byteBuffer).cleaner().clean();
}
}
Netty 中的堆外內(nèi)存池就是使用反射來(lái)實(shí)現(xiàn)手動(dòng)回收方式進(jìn)行回收的。