堆外內存和堆內內存

堆內內存

堆外內存和堆內內存是相對的二個概念,其中堆內內存是我們平常工作中接觸比較多的,我們在jvm參數中只要使用-Xms,-Xmx等參數就可以設置堆的大小和最大值,理解jvm的堆還需要知道下面這個公式:

堆內內存 = 新生代+老年代+持久代

在使用堆內內存(on-heap memory)的時候,完全遵守JVM虛擬機的內存管理機制,採用垃圾回收器(GC)統一進行內存管理,GC會在某些特定的時間點進行一次徹底回收,也就是Full GC,GC會對所有分配的堆內內存進行掃描,在這個過程中會對JAVA應用程序的性能造成一定影響,還可能會產生Stop The World。

 

堆外外存

和堆內內存相對應,堆外內存就是把內存對象分配在Java虛擬機的堆以外的內存,這些內存直接受操作系統管理(而不是虛擬機),這樣做的結果就是能夠在一定程度上減少垃圾回收對應用程序造成的影響。

代碼中能直接操作本地內存的方式有2種:使用未公開的Unsafe和NIO包下ByteBuffer。

作爲JAVA開發者我們經常用java.nio.DirectByteBuffer對象進行堆外內存的管理和使用,它會在對象創建的時候就分配堆外內存。

DirectByteBuffer類是在Java Heap外分配內存,對堆外內存的申請主要是通過成員變量unsafe來操作

 

堆外內存釋放

JDK中使用DirectByteBuffer對象來表示堆外內存,每個DirectByteBuffer對象在初始化時,都會創建一個對應的Cleaner對象,用於保存堆外內存的元信息(開始地址、大小和容量等),當DirectByteBuffer被GC回收後,Cleaner對象被放入ReferenceQueue中,然後由ReferenceHandler守護線程調用unsafe.freeMemory(address),回收堆外內存。 在Cleaner 內部中通過一個列表,維護了一個針對每一個 directBuffer 的一個回收堆外內存的 線程對象(Runnable),回收操作是發生在 Cleaner 的 clean() 方法中。

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

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

主動回收(推薦): 對於Sun的JDK,只要從DirectByteBuffer裏取出那個sun.misc.Cleaner,然後調用它的clean()就行;
基於 GC 回收:堆內的DirectByteBuffer對象被GC時,會調用cleaner回收其引用的堆外內存

 

爲什麼Cleaner對象能夠被放入ReferenceQueue中?

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

 

堆外內存注意

java.nio.DirectByteBuffer對象在創建過程中會先通過Unsafe接口直接通過os::malloc來分配內存,然後將內存的起始地址和大小存到java.nio.DirectByteBuffer對象,這樣就可以直接操作這些內存。這些內存只有在DirectByteBuffer回收掉之後纔有機會被回收。

當我們基於GC回收時,YGC只會將將新生代裏的不可達的DirectByteBuffer對象及其堆外內存回收,如果有大量的DirectByteBuffer對象移到了old區,但是又一直沒有做CMS GC或者FGC,而只進行YGC,物理內存會被慢慢耗光,觸發OutOfMemoryError。

因此爲了避免這種悲劇的發生,通過-XX:MaxDirectMemorySize來指定最大的堆外內存大小,當使用達到了閾值的時候將調用System.gc來做一次full gc,以此來回收掉沒有被使用的堆外內存。

 

System.gc的作用有哪些

1.做一次full gc

2.執行後會暫停整個進程。

3.System.gc我們可以禁掉,使用-XX:+DisableExplicitGC。

4.其實一般在cms gc下我們通過-XX:+ExplicitGCInvokesConcurrent也可以做稍微高效一點的gc,也就是並行gc。

5.最常見的場景是RMI/NIO下的堆外內存分配等

注:
如果我們使用了堆外內存,並且用了DisableExplicitGC設置爲true,那麼就是禁止使用System.gc,這樣堆外內存將無從觸發極有可能造成內存溢出錯誤,在這種情況下可以考慮使用ExplicitGCInvokesConcurrent參數。

說起Full gc我們最先想到的就是stop thd world,這裏要先提到VMThread,在jvm裏有這麼一個線程不斷輪詢它的隊列,這個隊列裏主要是存一些VM_operation的動作,比如最常見的就是內存分配失敗要求做GC操作的請求等,在對gc這些操作執行的時候會先將其他業務線程都進入到安全點,也就是這些線程從此不再執行任何字節碼指令,只有當出了安全點的時候才讓他們繼續執行原來的指令,因此這其實就是我們說的stop the world(STW),整個進程相當於靜止了。
 

 

堆外內存優缺點

優點

提升了IO效率(避免了數據從用戶態向內核態的拷貝).

對垃圾回收停頓的改善因爲full gc意味着徹底回收,徹底回收時,垃圾收集器會對所有分配的堆內內存進行完整的掃描,這意味着一個重要的事實——這樣一次垃圾收集對Java應用造成的影響,跟堆的大小是成正比的。過大的堆會影響Java應用的性能。如果使用堆外內存的話,堆外內存是直接受操作系統管理( 而不是虛擬機 )。這樣做的結果就是能保持一個較小的堆內內存,以減少垃圾收集對應用的影響。

可以在進程間共享,減少JVM間的對象複製,使得JVM的分割部署更容易實現

可以擴展至更大的內存空間。比如超過1TB甚至比主存還大的空間

它的持久化存儲可以支持快速重啓,同時還能夠在測試環境中重現生產數據

缺點

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

堆外內存的泄漏問題

堆外內存的數據結構問題:堆外內存最大的問題就是你的數據結構變得不那麼直觀,如果數據結構比較複雜,就要對它進行串行化(serialization),而串行化本身也會影響性能。另一個問題是由於你可以使用更大的內存,你可能開始擔心虛擬內存(即硬盤)的速度對你的影響了。

 

爲什麼堆外內存能夠提升IO效率?

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

 

堆外內存 VS 內存池

內存池:主要用於兩類對象:①生命週期較短,且結構簡單的對象,在內存池中重複利用這些對象能增加CPU緩存的命中率,從而提高性能;②加載含有大量重複對象的大片數據,此時使用內存池能減少垃圾回收的時間。

堆外內存:它和內存池一樣,也能縮短垃圾回收時間,但是它適用的對象和內存池完全相反。內存池往往適用於生命期較短的可變對象,而生命期中等或較長的對象,正是堆外內存要解決的。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章