堆外內存整理

堆外內存, JDK 1.4 nio引進了ByteBuffer.allocateDirect()分配堆外內存

  • ByteBuffer
    public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
    }

  • DirectByteBuffer
    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);//用Bits類保存總分配內存(按頁分配)的大小和實際內存的大小
    long base = 0;
    try {//在堆外內存的基地址,指定內存大小
    base = unsafe.allocateMemory(size);//unsafe.cpp中調用os::malloc分配內存
    } 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;
    }

  • Deallocator
    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);//調用OS的方法釋放地址,os::free
    address = 0;
    Bits.unreserveMemory(size, capacity);//統計堆外內存大小
    }
    }

  • Cleaner
    public class Cleaner extends PhantomReference<Object> {
    private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue();//static數據
    private static Cleaner first = null;//static數據
    private Cleaner next = null;
    private Cleaner prev = null;
    private final Runnable thunk;//Deallocator對象,每個cleaner對象都保留了一個Deallocator對象,它里面有address基地址等
    private static synchronized Cleaner add(Cleaner var0) {
    if(first != null) {
    var0.next = first;
    first.prev = var0;
    }
    first = var0;
    return var0;
    }
    private Cleaner(Object var1, Runnable var2) {
    super(var1, dummyQueue);//var1 傳的是DirectByteBuffer對象
    this.thunk = var2;//Deallocator對象
    }
    public static Cleaner create(Object var0, Runnable var1) {
    return var1 == null?null:add(new Cleaner(var0, var1));//var0傳的是DirectByteBuffer對象
    }

  • Bits
    // -- Direct memory management --
    // A user-settable upper limit on the maximum amount of allocatable direct buffer memory.
    // This value may be changed during VM initialization if it is launched with "-XX:MaxDirectMemorySize=<size>".
    private static volatile long maxMemory = VM.maxDirectMemory();
    private static volatile long reservedMemory;
    private static volatile long totalCapacity;
    private static volatile long count;
    private static boolean memoryLimitSet = false;
    // These methods should be called whenever direct memory is allocated or
    // freed. They allow the user to control the amount of direct memory
    // which a process may access. All sizes are specified in bytes.
    static void reserveMemory(long size, int cap) {
    synchronized (Bits.class) {
    if (!memoryLimitSet && VM.isBooted()) {
    maxMemory = VM.maxDirectMemory();// 67108864L == 64MB
    memoryLimitSet = true;
    }
    // -XX:MaxDirectMemorySize limits the total capacity rather than the
    // actual memory usage, which will differ when buffers are page aligned.
    if (cap <= maxMemory - totalCapacity) {
    reservedMemory += size;
    totalCapacity += cap;
    count++;
    return;
    }
    }
    System.gc();//內存不夠了, try gc
    try {
    Thread.sleep(100);
    } catch (InterruptedException x) {
    // Restore interrupt status
    Thread.currentThread().interrupt();
    }
    synchronized (Bits.class) {
    if (totalCapacity + cap > maxMemory)
    throw new OutOfMemoryError("Direct buffer memory");
    reservedMemory += size;
    totalCapacity += cap;
    count++;
    }
    }
    static synchronized void unreserveMemory(long size, int cap) {
    if (reservedMemory > 0) {
    reservedMemory -= size;
    totalCapacity -= cap;
    count--;
    assert (reservedMemory > -1);
    }
    }

  • DirectByteBuffer被回收

    DirectByteBuffer對象在創建的時候關聯了一個PhantomReference,說到PhantomReference它其實主要是用來跟蹤對象何時被回收的,
    它不能影響gc決策,但是gc過程中如果發現某個對象除了只有PhantomReference引用它之外,并沒有其他的地方引用它了,
    那將會把這個引用(Cleaner)放到java.lang.ref.Reference.pending隊列里,
    在gc完畢的時候通知ReferenceHandler這個守護線程去執行一些后置處理,
    而DirectByteBuffer關聯的PhantomReference是PhantomReference的一個子類,
    在最終的處理里會通過Unsafe的free接口來釋放DirectByteBuffer對應的堆外內存塊

  • JDK里面的ReferenceHandler實現
    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();//直接調用clean方法清理
    continue;
    }
    ReferenceQueue q = r.queue;
    if (q != ReferenceQueue.NULL) q.enqueue(r);
    }
    }
    }

  • 簡單流程梳理

    • 堆外內存的申請
      • ByteBuffer.allocateDirect()
      • unsafe.allocateMemory()
      • os::malloc()
    • 堆外內存的釋放
      • cleaner.clean()
        • 把自身從Clener鏈表刪除,從而在下次GC時能夠被回收
        • 釋放堆外內存
      • unsafe.freeMemory()
      • os::free()
  • 對象的引用關系

    • 初始化時
    • 如果該DirectByteBuffer對象在一次GC中被回收了
  • 不過很多線上環境的JVM參數有-XX:+DisableExplicitGC,導致了System.gc()等于一個空函數,根本不會觸發FGC,這一點在使用Netty框架時需要注意是否會出問題

  • 關于直接內存默認值是否為64MB?

    • java.lang.System
      private static void initializeSystemClass() {//Initialize the system class. Called after thread initialization.
      ...
      sun.misc.VM.saveAndRemoveProperties(props);
      ...
      }
    • saveAndRemoveProperties(){
      // Set the maximum amount of direct memory. This value is controlled
      // by the vm option -XX:MaxDirectMemorySize=<size>.
      // The maximum amount of allocatable direct buffer memory (in bytes)
      // from the system property sun.nio.MaxDirectMemorySize set by the VM.
      // The system property will be removed.
      String s = (String)props.remove("sun.nio.MaxDirectMemorySize");
      if (s != null) {
      if (s.equals("-1")) {
      // -XX:MaxDirectMemorySize not given, take default
      directMemory = Runtime.getRuntime().maxMemory();
      } else {
      long l = Long.parseLong(s);
      if (l > -1)
      directMemory = l;
      }
      }}
    • 如果我們通過-Dsun.nio.MaxDirectMemorySize指定了這個屬性,只要它不等于-1,那效果和加了-XX:MaxDirectMemorySize一樣的,如果兩個參數都沒指定,那么最大堆外內存的值來自于directMemory = Runtime.getRuntime().maxMemory(),這是一個native方法
    • Universe::heap()->max_capacity();
    • 其中在我們使用CMS GC的情況下的實現如下,其實是新生代的最大值-一個survivor的大小+老生代的最大值,也就是我們設置的-Xmx的值里除去一個survivor的大小就是默認的堆外內存的大小了
  • 如果發現某個對象除了只有PhantomReference引用它之外,并沒有其他的地方引用它了,那將會把這個引用放到java.lang.ref.Reference.pending隊列里,在gc完畢的時候通知ReferenceHandler這個守護線程去執行一些后置處理

  • 可見如果pending為空的時候,會通過lock.wait()一直等在那里,其中喚醒的動作是在jvm里做的,當gc完成之后會調用如下的方法VM_GC_Operation::doit_epilogue(),在方法末尾會調用lock的notify操作,至于pending隊列什么時候將引用放進去的,其實是在gc的引用處理邏輯中放進去的,針對引用的處理后面可以專門寫篇文章來介紹

  • 對于System.gc的實現,它會對新生代和老生代都會進行內存回收,這樣會比較徹底地回收DirectByteBuffer對象以及他們關聯的堆外內存,我們dump內存發現DirectByteBuffer對象本身其實是很小的,但是它后面可能關聯了一個非常大的堆外內存,因此我們通常稱之為『冰山對象』,我們做ygc的時候會將新生代里的不可達的DirectByteBuffer對象及其堆外內存回收了,但是無法對old里的DirectByteBuffer對象及其堆外內存進行回收,這也是我們通常碰到的最大的問題,如果有大量的DirectByteBuffer對象移到了old,但是又一直沒有做cms gc或者full gc,而只進行ygc,那么我們的物理內存可能被慢慢耗光,但是我們還不知道發生了什么,因為heap明明剩余的內存還很多(前提是我們禁用了System.gc)。

  • 我們通信過程中如果數據是在Heap里的,最終也還是會copy一份到堆外,然后再進行發送,所以為什么不直接使用堆外內存呢

  • gc機制與堆外內存的關系也說了,如果一直觸發不了cms gc或者full gc,那么后果可能很嚴重

  • References

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,362評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,577評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,486評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,852評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,600評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,944評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,944評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,108評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,652評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,385評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,616評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,111評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,798評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,205評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,537評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,334評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,570評論 2 379

推薦閱讀更多精彩內容