1. 程式人生 > >後端---java中堆外記憶體詳解

後端---java中堆外記憶體詳解

堆外記憶體和堆內記憶體

   堆外記憶體又稱為直接記憶體(Direct Memory)並不是虛擬機器執行時資料區的一部分,也不是Java虛擬機器規範中定義的記憶體區域.一直以來是Javaer們難以關注的一片領域,今天我們就一起探索一下這片區域究竟隱藏著什麼東東????

    JVM可以使用的記憶體分外2種:堆記憶體和堆外記憶體.

我們先看一下我們已經相對來說十分熟悉的堆內記憶體:

Java堆(JAva Heap)是Java虛擬機器所管理的記憶體中的最大的一塊.Java堆是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立.此記憶體區域的唯一目的就是存放物件例項,幾乎所有的物件例項都在這裡分配記憶體.這一點Java虛擬機器所規範中的描述是:所有的物件例項以及陣列都要在堆上分配,但是隨著JIT編譯器的發展與逃逸分析技術逐漸成熟,棧上分配,標量替換優化技術將會導致一些微妙的變化發生,所有的物件都分配在堆上也漸漸變得不是那麼"絕對"了,所有有了堆外記憶體的概念.

  •    堆記憶體完全由JVM負責分配和釋放,如果程式沒有缺陷程式碼導致記憶體洩露,那麼就不會遇到java.lang.OutOfMemoryError這個錯誤。
  •     使用堆外記憶體,就是為了能直接分配和釋放記憶體,提高效率。JDK5.0之後,程式碼中能直接操作本地記憶體的方式有2種:使用未公開的Unsafe和NIO包下ByteBuffer。

我們一起來看看NIO中提供的ByteBuffer:

我們將最大堆外記憶體設定成40M,執行這段程式碼會發現:程式可以一直執行下去,不會報OutOfMemoryError。如果使用了-verbose:gc -XX:+PrintGCDetails,會發現程式頻繁的進行垃圾回收活動。那麼DirectByteBuffer究竟是如何釋放堆外記憶體的?

    我們修改下JVM的啟動引數,重新執行之前的程式碼:

與之前的JVM啟動引數相比,增加了-XX:+DisableExplicitGC,這個引數作用是禁止程式碼中顯示呼叫GC。程式碼如何顯示呼叫GC呢,通過System.gc()函式呼叫。如果加上了這個JVM啟動引數,那麼程式碼中呼叫System.gc()沒有任何效果,相當於是沒有這行程式碼一樣。

顯然堆記憶體(包括新生代和老年代)記憶體很充足,但是堆外記憶體溢位了。也就是說NIO直接記憶體的回收,需要依賴於System.gc()。如果我們的應用中使用了java nio中的direct memory,那麼使用-XX:+DisableExplicitGC一定要小心,存在潛在的記憶體洩露風險

。 

 我們知道java程式碼無法強制JVM何時進行垃圾回收,也就是說垃圾回收這個動作的觸發,完全由JVM自己控制,它會挑選合適的時機回收堆記憶體中的無用java物件。程式碼中顯示呼叫System.gc(),只是建議JVM進行垃圾回收,但是到底會不會執行垃圾回收是不確定的,可能會進行垃圾回收,也可能不會。什麼時候才是合適的時機呢?一般來說是,系統比較空閒的時候(比如JVM中活動的執行緒很少的時候),還有就是記憶體不足,不得不進行垃圾回收。我們例子中的根本矛盾在於:堆記憶體由JVM自己管理,堆外記憶體必須要由我們自己釋放;堆記憶體的消耗速度遠遠小於堆外記憶體的消耗,但要命的是必須先釋放堆記憶體中的物件,才能釋放堆外記憶體,但是我們又不能強制JVM釋放堆記憶體。

 Direct Memory的回收機制:Direct Memory是受GC控制的,例如ByteBuffer bb = ByteBuffer.allocateDirect(1024),這段程式碼的執行會在堆外佔用1k的記憶體,Java堆內只會佔用一個物件的指標引用的大小,堆外的這1k的空間只有當bb物件被回收時,才會被回收,這裡會發現一個明顯的不對稱現象,就是堆外可能佔用了很多,而堆內沒佔用多少,導致還沒觸發GC,那就很容易出現Direct Memory造成實體記憶體耗光。

 Direct ByteBuffer分配出去的記憶體其實也是由GC負責回收的,而不像Unsafe是完全自行管理的,Hotspot在GC時會掃描Direct ByteBuffer物件是否有引用,如沒有則同時也會回收其佔用的堆外記憶體。

使用堆外記憶體與物件池都能減少GC的暫停時間,這是它們唯一的共同點。生命週期短的可變物件,建立開銷大,或者生命週期雖長但存在冗餘的可變物件都比較適合使用物件池。生命週期適中,或者複雜的物件則比較適合由GC來進行處理。然而,中長生命週期的可變物件就比較棘手了,堆外記憶體則正是它們的菜。

堆外記憶體的好處是:

(1)可以擴充套件至更大的記憶體空間。比如超過1TB甚至比主存還大的空間;

(2)理論上能減少GC暫停時間;

(3)可以在程序間共享,減少JVM間的物件複製,使得JVM的分割部署更容易實現;

(4)它的持久化儲存可以支援快速重啟,同時還能夠在測試環境中重現生產資料

站在系統設計的角度來看,使用堆外記憶體可以為你的設計提供更多可能。最重要的提升並不在於效能

為什麼堆外記憶體能夠提升IO效率?

  堆內記憶體由JVM管理,屬於“使用者態”;而堆外記憶體由OS管理,屬於“核心態”。如果從堆內向磁碟寫資料時,資料會被先複製到堆外記憶體,即核心緩衝區,然後再由OS寫入磁碟,使用堆外記憶體避免了資料從使用者內向核心態的拷貝。

使用堆外記憶體的原因

  • 對垃圾回收停頓的改善
    因為full gc 意味著徹底回收,徹底回收時,垃圾收集器會對所有分配的堆內記憶體進行完整的掃描,這意味著一個重要的事實——這樣一次垃圾收集對Java應用造成的影響,跟堆的大小是成正比的。過大的堆會影響Java應用的效能。如果使用堆外記憶體的話,堆外記憶體是直接受作業系統管理( 而不是虛擬機器 )。這樣做的結果就是能保持一個較小的堆內記憶體,以減少垃圾收集對應用的影響。
  • 在某些場景下可以提升程式I/O操縱的效能。少去了將資料從堆內記憶體拷貝到堆外記憶體的步驟。

什麼情況下使用堆外記憶體

  • 堆外記憶體適用於生命週期中等或較長的物件。( 如果是生命週期較短的物件,在YGC的時候就被回收了,就不存在大記憶體且生命週期較長的物件在FGC對應用造成的效能影響 )。
  • 直接的檔案拷貝操作,或者I/O操作。直接使用堆外記憶體就能少去記憶體從使用者記憶體拷貝到系統記憶體的操作,因為I/O操作是系統核心記憶體和裝置間的通訊,而不是通過程式直接和外設通訊的。
  • 同時,還可以使用 池+堆外記憶體 的組合方式,來對生命週期較短,但涉及到I/O操作的物件進行堆外記憶體的再使用。( Netty中就使用了該方式 )

堆外記憶體的特點

  • 對於大記憶體有良好的伸縮性
  • 對垃圾回收停頓的改善可以明顯感覺到
  • 在程序間可以共享,減少虛擬機器間的複製
     

堆外記憶體的一些問題

  • 堆外記憶體回收問題,以及堆外記憶體的洩漏問題。這個在上面的原始碼解析已經提到了
  • 堆外記憶體的資料結構問題:堆外記憶體最大的問題就是你的資料結構變得不那麼直觀,如果資料結構比較複雜,就要對它進行序列化(serialization),而序列化本身也會影響效能。另一個問題是由於你可以使用更大的記憶體,你可能開始擔心虛擬記憶體(即硬碟)的速度對你的影響了。