堆外內存總結


前言

  上次網易一面面試官提到了“是否了解堆外內存?”、“堆外內存是否需要手動釋放?”等問題,那時候我誤以為所提到的“堆外內存”是指元空間這個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信息一般生命周期也都較長。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM