前言
上次網易一面面試官提到了“是否了解堆外內存?”、“堆外內存是否需要手動釋放?”等問題,那時候我誤以為所提到的“堆外內存”是指元空間這個jvm管理的堆外內存,對於元空間是否手動釋放這樣的問題就令我十分疑惑,按理說當元空間的類信息會在類被定義成“無用的類”時會被回收,因此不需要我們手動釋放,然后面試小哥又重復了一遍我的回答“不需要手動釋放嗎?”,我只能回答對此可能不是很了解。
面試結束后上網搜索了一下,他想要問的應該是java中的DirectByteBuffer,而今天早上又看到一篇博客“堆外內存泄漏的排查過程”。就打算今天對堆外內存做一個總結。
使用
DirectByteBuffer的創建非常簡單,使用ByteBuffer的靜態方法
1 ByteBuffer dirBuf = ByteBuffer.allocateDirect(capacity);
就可以創建一個DirectByteBuffer,與普通的ByteBuffer不一樣的地方在於一個是在jvm堆內存中,另一個不在jvm堆內存中。
創建、清理
為了了解堆外內存是如何被回收的,我們先來看allocateDirect這個方法是如何創建一個實例的。
1 DirectByteBuffer(int cap) { // package-private 2 3 super(-1, 0, cap, cap); 4 boolean pa = VM.isDirectMemoryPageAligned(); 5 int ps = Bits.pageSize(); 6 long size = Math.max(1L, (long)cap + (pa ? ps : 0)); 7 Bits.reserveMemory(size, cap);//1.1預定一塊空間 8 9 long base = 0; 10 try { 11 base = unsafe.allocateMemory(size);//1.2創建 12 } catch (OutOfMemoryError x) { 13 Bits.unreserveMemory(size, cap); 14 throw x; 15 } 16 unsafe.setMemory(base, size, (byte) 0); 17 if (pa && (base % ps != 0)) { 18 // Round up to page boundary 19 address = base + ps - (base & (ps - 1)); 20 } else { 21 address = base; 22 } 23 cleaner = Cleaner.create(this, new Deallocator(base, size, cap));//2.構造一個Cleaner對象 24 att = null; 25 26 }
先進入(注釋1.1)Bits.reserveMemory(size, cap)這個方法,它主要是預申請一塊空間,size是系統的頁大小。
1 static void reserveMemory(long size, int cap) { 2 3 if (!memoryLimitSet && VM.isBooted()) { 4 maxMemory = VM.maxDirectMemory(); 5 memoryLimitSet = true; 6 } 7 8 // optimist! 9 if (tryReserveMemory(size, cap)) { 10 return; 11 } 12 13 final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess(); 14 15 // retry while helping enqueue pending Reference objects 16 // which includes executing pending Cleaner(s) which includes 17 // Cleaner(s) that free direct buffer memory 18 while (jlra.tryHandlePendingReference()) { 19 if (tryReserveMemory(size, cap)) { 20 return; 21 } 22 } 23 24 // trigger VM's Reference processing 25 System.gc(); 26 27 // a retry loop with exponential back-off delays 28 // (this gives VM some time to do it's job) 29 boolean interrupted = false; 30 try { 31 long sleepTime = 1; 32 int sleeps = 0; 33 while (true) { 34 if (tryReserveMemory(size, cap)) { 35 return; 36 } 37 if (sleeps >= MAX_SLEEPS) { 38 break; 39 } 40 if (!jlra.tryHandlePendingReference()) { 41 try { 42 Thread.sleep(sleepTime); 43 sleepTime <<= 1; 44 sleeps++; 45 } catch (InterruptedException e) { 46 interrupted = true; 47 } 48 } 49 } 50 51 // no luck 52 throw new OutOfMemoryError("Direct buffer memory"); 53 54 } finally { 55 if (interrupted) { 56 // don't swallow interrupts 57 Thread.currentThread().interrupt(); 58 } 59 } 60 }
reserveMemory方法流程:
1、首先是第八行的tryReserveMemory方法,嘗試申請空間
2、申請失敗就到第18-22行嘗試清理堆外內存,再tryReserveMemory
3、如果清理完了還是申請失敗,就調用System.gc(),觸發full gc,觸發后,可以清理老年代(新生代也可能有但是大多是在老年代的)的堆外內存的引用(如果存在應該被清理的引用),清理完后就是剩下的部分,33-53行自旋MAX_SLEEPS次,sleep等待full gc的觸發(System.gc()可能有延時),如果次數大於MAX_SLEEPS還沒申請成功,就拋出異常。
接下來是(注釋1.2)的unsafe.allocateMemory真正創建堆外內存空間。
再之后是(注釋2)創建Cleaner對象,用於之后清理這塊堆外內存空間。
Cleaner繼承PhantomReference類,並通過自身的next和prev字段維護的一個雙向鏈表,當DirectByteBuffer對象從“pending” 變為 “enqueue”時(即gc過程中對象只有被虛引用,這個引用會被放到java.lang.ref.Reference.pending隊列里,調用ReferenceHandler的run中不斷自旋的tryHandlePending(true)方法處理,清理pending鏈,使用clean方法將堆外內存清理掉)。
參數設置
我們可以通過-XX:MaxDirectMemorySize來指定最大的堆外內存大小,當使用達到了閾值的時候將調用System.gc()來做一次full gc,以此來回收掉沒有被使用的堆外內存,這個是jvm底層幫我們做的,我們只需要設定其參數即可。
使用情景
1、直接的文件拷貝操作,或者I/O操作。當操作系統對堆內內存進行文件拷貝、io處理時先會拷貝一份到堆外,再進行發送處理,而堆外內存就少了一個拷貝的耗時。
2、堆外內存適用於生命周期較長的對象,不會占用堆內的內存,這點與元空間的出現原因類似,Class信息一般生命周期也都較長。