堆外內存

堆外內存定義java

  建立Java.nio.DirectByteBuffer時分配的內存。緩存

堆外內存優缺點框架

  優勢: 提高了IO效率(避免了數據從用戶態向內核態的拷貝);減小了GC次數(節約了大量的堆內內存)。性能

  缺點:分配和回收堆外內存比分配和回收堆內存耗時;(解決方案:經過對象池避免頻繁地建立和銷燬堆外內存)線程

爲何堆外內存可以提高IO效率?調試

  堆內內存由JVM管理,屬於「用戶態」;而堆外內存由OS管理,屬於「內核態」。若是從堆內向磁盤寫數據時,數據會被先複製到堆外內存,即內核緩衝區,而後再由OS寫入磁盤,使用堆外內存避免了數據從用戶內向內核態的拷貝。日誌

堆外內存申請netty

  JDK的ByteBuffer類提供了一個接口allocateDirect(int capacity)進行堆外內存的申請,底層經過unsafe.allocateMemory(size)實現。Netty、Mina等框架提供的接口也是基於ByteBuffer封裝的。對象

堆外內存釋放blog

  JDK中使用DirectByteBuffer對象來表示堆外內存,每一個DirectByteBuffer對象在初始化時,都會建立一個對應的Cleaner對象,用於保存堆外內存的元信息(開始地址、大小和容量等),當DirectByteBuffer被GC回收後,Cleaner對象被放入ReferenceQueue中,而後由ReferenceHandler守護線程調用unsafe.freeMemory(address),回收堆外內存。

 主動回收(推薦): 對於Sun的JDK,只要從DirectByteBuffer裏取出那個sun.misc.Cleaner,而後調用它的clean()就行;

  基於 GC 回收:堆內的DirectByteBuffer對象被GC時,會調用cleaner回收其引用的堆外內存。問題是YGC只會將將新生代裏的不可達的DirectByteBuffer對象及其堆外內存回收,若是有大量的DirectByteBuffer對象移到了old區,可是又一直沒有作CMS GC或者FGC,而只進行YGC,物理內存會被慢慢耗光,觸發OOM;

堆外內存溢出

  Java.nio.DirectByteBuffer所需的內存超過了物理分配的堆外內存,出現」java.lang.OutOfMemoryError: Direct buffer memory」。

堆外內存使用注意

  java.nio.DirectByteBuffer對象在建立過程當中會先經過Unsafe接口直接經過os::malloc來分配內存,而後將內存的起始地址和大小存到java.nio.DirectByteBuffer對象裏,這樣就能夠直接操做這些內存。這些內存只有在DirectByteBuffer回收掉以後纔有機會被回收,所以若是這些對象大部分都移到了old,可是一直沒有觸發CMS GC或者Full GC,那麼悲劇將會發生,由於你的物理內存被他們耗盡了,所以爲了不這種悲劇的發生,經過-XX:MaxDirectMemorySize來指定最大的堆外內存大小,當使用達到了閾值的時候將調用System.gc來作一次full gc,以此來回收掉沒有被使用的堆外內存。

堆外內存默認大小

  堆外內存默認值: (-Xmx值) - (1個survivor大小)

爲何Cleaner對象可以被放入ReferenceQueue中?

  Cleaner對象關聯了一個PhantomReference引用,若是GC過程當中某個對象除了只有PhantomReference引用它以外,並無其餘地方引用它了,那將會把這個引用放到java.lang.ref.Reference.pending隊列裏,在GC完畢的時候通知ReferenceHandler這個守護線程去執行一些後置處理,在最終的處理裏會經過Unsafe的free接口來釋放DirectByteBuffer對應的堆外內存塊。

 

Java的虛擬機對內存的管理大部分狀況下就是指堆內存的管理, GC的也是對堆內存的清理和回收.

 

 netty中提到的一個概念, "零拷貝", 也是netty高性能的緣由之一. 零拷貝, 主要體如今三個方面:

    Netty的接收和發送ByteBuffer採用DIRECT BUFFERS,使用堆外(直接)內存進行Socket讀寫,不須要進行字節緩衝區的二次拷貝。若是使用傳統的堆內存(HEAP BUFFERS)進行Socket讀寫,JVM會將堆內Buffer拷貝一份到直接內存中,而後才寫入Socket中。相比於堆外直接內存,消息在發送過程當中多了一次緩衝區的內存拷貝。

    Netty提供了組合Buffer對象,能夠聚合多個ByteBuffer對象,用戶能夠像操做一個Buffer那樣方便的對組合Buffer進行操做,避免了傳統經過內存拷貝的方式將幾個小Buffer合併成一個大的Buffer。

    Netty的文件傳輸採用了transferTo方法,它能夠直接將文件緩衝區的數據發送到目標Channel,避免了傳統經過循環write方式致使的內存拷貝問題。

在使用堆外內存的同時也帶來了新的問題, 相比較堆內存, 堆外內存的分配和回收要更耗時, 因此netty提供了基於內存池的緩衝區重用機制.

 

將本地緩存和堆外內存聯繫到一塊兒, 是有一次調試線上頻繁FULL GC而後OOM的問題, 當時的狀況是, 線上頻繁的報警有FULLGC, 懷疑有內存泄露,, 經過dump堆內存快照, 分析後發現有一個特別大的HashMap, 緣由是打點日誌引發的, 應用默認集成了日誌系統會自動記錄用戶的全部行爲, 應用這邊合併日誌後發送到日誌接收端, 日誌接收端掛掉了, 致使一直在重試, 重試的過程當中不停的有新的日誌加進來, 最後致使FULLGC. 當時我就在想, 若是將這種本地緩存移到堆外是否是就能夠不用參與GC, 也可使用更大的內存.

堆外內存有如下特色:

    對於大內存有良好的伸縮性, 堆外內存突破JVM的內存限制

    對垃圾回收停頓的改善能夠明顯感受到

    在進程間能夠共享,減小虛擬機間的複製

堆外內存更適合生命週期中等或長期的對象

 

關於堆外內存的回收

堆外內存的回收其實依賴於咱們的GC機制(堆外內存不會對GC形成什麼影響)

首先咱們要知道在java層面和咱們在堆外分配的這塊內存關聯的只有與之關聯的DirectByteBuffer對象了,它記錄了這塊內存的基地址以及大小,那麼既然和GC也有關,那就是GC能經過操做DirectByteBuffer對象來間接操做對應的堆外內存了。

DirectByteBuffer對象在建立的時候關聯了一個PhantomReference,說到PhantomReference它其實主要是用來跟蹤對象什麼時候被回收的,它不能影響GC決策.

GC過程當中若是發現某個對象除了只有PhantomReference引用它以外,並無其餘的地方引用它了,那將會把這個引用放到java.lang.ref.Reference.pending隊列裏,在GC完畢的時候通知ReferenceHandler這個守護線程去執行一些後置處理, 而DirectByteBuffer關聯的PhantomReference是PhantomReference的一個子類,在最終的處理裏會經過Unsafe的free接口來釋放DirectByteBuffer對應的堆外內存塊

 

    什麼是基地址 這裏就提出了段的概念, 將1G的數據劃分爲n個段, 每個段是64K, 每個段也就是每個64K就是一個基地址 段內的數據的地址就是當前基地址的偏移地址, 此時 段地址+偏移地址就可以找到真正的內存數據了.

 

爲何要主動調用System.gc

System.gc()會對新生代的老生代都會進行內存回收,這樣會比較完全地回收DirectByteBuffer對象以及他們關聯的堆外內存.

DirectByteBuffer對象自己實際上是很小的,可是它後面可能關聯了一個很是大的堆外內存,所以咱們一般稱之爲*冰山對象.

咱們作ygc的時候會將新生代裏的不可達的DirectByteBuffer對象及其堆外內存回收了,可是沒法對old裏的DirectByteBuffer對象及其堆外內存進行回收,這也是咱們一般碰到的最大的問題.

若是有大量的DirectByteBuffer對象移到了old,可是又一直沒有作cms gc或者full gc,而只進行ygc,那麼咱們的物理內存可能被慢慢耗光,可是咱們還不知道發生了什麼,由於heap明明剩餘的內存還不少(前提是咱們禁用了System.gc -- JVM參數DisableExplicitGC)。

相關文章
相關標籤/搜索