2025-04-23

廣義的堆外內存

說到堆外內存,那大傢肯定想到堆內內存,這也是我們大傢接觸最多的,我們在jvm參數裡通常設置-Xmx來指定我們的堆的最大值,不過這還不是我們理解的Java堆,-Xmx的值是新生代和老生代的和的最大值,我們在jvm參數裡通常還會加一個參數-XX:MaxPermSize來指定持久代的最大值,那麼我們認識的Java堆的最大值其實是-Xmx和-XX:MaxPermSize的總和,在分代算法下,新生代,老生代和持久代是連續的虛擬地址,因為它們是一起分配的,那麼剩下的都可以認為是堆外內存(廣義的)瞭,這些包括瞭jvm本身在運行過程中分配的內存,codecache,jni裡分配的內存,DirectByteBuffer分配的內存等等

狹義的堆外內存

而作為java開發者,我們常說的堆外內存溢出瞭,其實是狹義的堆外內存,這個主要是指java.nio.DirectByteBuffer在創建的時候分配內存,我們這篇文章裡也主要是講狹義的堆外內存,因為它和我們平時碰到的問題比較密切

JDK/JVM裡DirectByteBuffer的實現?

DirectByteBuffer通常用在通信過程中做緩沖池,在mina,netty等nio框架中屢見不鮮

通過上面的代碼我們知道可以通過-XX:MaxDirectMemorySize來指定最大的堆外內存

DirectByteBuffer在創建的時候會通過Unsafe的native方法來直接使用malloc分配一塊內存,這塊內存是heap 之外的,那麼自然也不會對gc造成什麼影響(System.gc除外),因為gc耗時的操作主要是操作heap之內的對象,對這塊內存的操作也是直接通過 Unsafe的native方法來操作的,相當於DirectByteBuffer僅僅是一個殼,還有我們通信過程中如果數據是在Heap裡的,最終也還是會copy一份到堆外,然後再進行發送,所以為什麼不直接使用堆外內存呢。對於需要頻繁操作的內存,並且僅僅是臨時存在一會的,都建議使用堆外內存,並且做成緩沖池,不斷循環利用這塊內存。?

如果我們大面積使用堆外內存並且沒有限制,那遲早會導致內存溢出,畢竟程式是跑在一臺資源受限的機器上,因為這塊內存的回收不是你直接能控制的。

正常情況下,JVM創建一個緩沖區的時候,實際上做瞭如下幾件事:

JVM確保Heap區域內的空間足夠,如果不夠則使用觸發GC在內的方法獲得空間;

獲得空間之後會找一組堆內的連續地址分配數組, 這裡需要註意的是,在物理內存上,這些字節是不一定連續的;

對於不涉及到IO的操作,這樣的處理沒有任何問題,但是當進行IO操作的時候就會出現一點性能問題.

所有的IO操作都需要操作系統進入內核態才行,而JVM進程屬於用戶態進程, 當JVM需要把一個緩沖區寫到某個Channel或Socket的時候,需要切換到內核態.

而內核態由於並不知道JVM裡面這個緩沖區存儲在物理內存的什麼地址,並且這些物理地址並不一定是連續的(或者說不一定是IO操作需要的塊結構),所以在切換之前JVM需要把緩沖區復制到物理內存一塊連續的內存上, 然後由內核去讀取這塊物理內存,整合成連續的、分塊的內存.

為瞭解決這個問題, Java的某些版本會把物理區域分配好的部分內存做緩存就不用每次都開辟一塊空間,但效果還不夠好,畢竟復制的部分是少不瞭的.

JDK1.4之後引入瞭NIO, 提供瞭一種內存映射技術, 讓我們可以直接從Java代碼中創建DirectBuffer,這種Buffer在創建的時候直接就在物理內存中分配一塊連續內存,當需要使用的時候不再需要復制,內核直接調用即可. 但缺點也是顯而易見的,就是每次分配都比較昂貴一點,同時由於分配的內存不在Java Heap中,所以也不會受用戶設置的堆大小的限制.

通常情況下,大量使用IO操作的時候使用內存映射是非常值得的

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *