還不知道Off-Heap堆外內(nèi)存?安排!
目錄
- on-heap堆內(nèi)內(nèi)存是什么?
- JVM 堆內(nèi)存是如何去劃分的?
- JVM 堆內(nèi)存滿了后會怎么樣?
- 基于堆外內(nèi)存解決系統(tǒng) GC 卡頓問題
今天給大家聊一個很有意思的知識,就是 off-heap 堆外內(nèi)存,平時出去面試,或者研究一些技術(shù)的時候,經(jīng)??赡軙龅?off-heap 堆外內(nèi)存這個東西,但是很多人可能還不知道 off-heap 堆外內(nèi)存到底是什么,所以今天就給大家來深入的分析一下。
on-heap 堆內(nèi)內(nèi)存是什么?
要說這個 off-heap 堆外內(nèi)存,就得先說 on-heap 也就是堆內(nèi)內(nèi)存,這個 on-heap 堆內(nèi)內(nèi)存相信很多人應(yīng)該都是熟悉的。
那就是咱們平時寫好的 Java 系統(tǒng)其實運(yùn)行起來就是一個 JVM 進(jìn)程,這個 JVM 進(jìn)程是有一塊內(nèi)存空間專門給他用的,這塊內(nèi)存空間就是堆內(nèi)內(nèi)存。
大概如下圖所示:
JVM 堆內(nèi)存是如何去劃分的?
那么這里通常會產(chǎn)生什么問題呢?一般來說沒什么大問題,但是如果是遇到要把大量數(shù)據(jù)緩存在 JVM 堆內(nèi)存里的時候,就可能會有問題了。
所謂的數(shù)據(jù)緩存,意思就是說,把很多數(shù)據(jù)存放在堆內(nèi)存里,這些數(shù)據(jù)是要一直用的,所以一般來說不能把他回收掉,所以會導(dǎo)致可能很多數(shù)據(jù)一直停留在 JVM 的堆內(nèi)存里。
如下圖:
那么下一個問題來了,這個 JVM 里的堆內(nèi)存是有劃分的,一塊區(qū)域是年輕代,一塊區(qū)域是老年代,像這種緩存數(shù)據(jù),因為是長期存在堆內(nèi)存里的,所以通常會在年輕代里待一段時間,然后因為沒法垃圾回收,給放到老年代里去。
此時如下圖:
JVM 堆內(nèi)存滿了后會怎么樣?
但是這個老年代里如果放了太多緩存數(shù)據(jù)以后,就可能會導(dǎo)致他剩余的可用空間就會比較少了,此時可能會導(dǎo)致老年代經(jīng)常會放一點別的數(shù)據(jù)就塞滿了,一旦塞滿了就會觸發(fā) JVM 的 Full GC,有一個垃圾回收線程會去回收老年代里的數(shù)據(jù)。
此時如下圖:
可是此時一般來說能回收的也就是除了緩存數(shù)據(jù)之外的一些空間,哪怕你回收了,但是緩存數(shù)據(jù)是要一直存在的,所以沒法回收掉。
此時會導(dǎo)致每次你回收了一部分剩余空間之后,然后還是剩余了很多緩存數(shù)據(jù),此時對于緩存數(shù)據(jù)來說會一直占據(jù)老年代的很大空間。
那么此時必然導(dǎo)致一個現(xiàn)象,那就是老年代會頻繁的寫一點數(shù)據(jù)就滿了,寫一點數(shù)據(jù)就滿了,然后一會兒就得觸發(fā)一下 Full GC。
每次 Full GC 都會導(dǎo)致 JVM 停止運(yùn)行,沒法處理外部請求,此時對外部來說,就會感覺你的系統(tǒng)性能經(jīng)常抖動,一會卡一下,一會兒卡一下。
所以往往來說,把很多數(shù)據(jù)緩存在 JVM 內(nèi)部,是很可能導(dǎo)致上述現(xiàn)象,就是老年代頻繁塞滿、頻繁觸發(fā) Full GC、頻繁導(dǎo)致系統(tǒng)停頓沒法處理請求。
如下圖:
基于堆外內(nèi)存解決系統(tǒng) GC 卡頓問題
所以針對這種情況,往往我們的優(yōu)化手段,就是會把要緩存的數(shù)據(jù),從 JVM 堆內(nèi)存里轉(zhuǎn)移到 offheap 堆外內(nèi)存里去,那所以問題來了,啥叫做堆外內(nèi)存呢?
就是顧名思義,不歸 JVM 管的內(nèi)存區(qū)域,OS 操作系統(tǒng)負(fù)責(zé)管理的一部分內(nèi)存,叫做堆外內(nèi)存。
所以我們其實可以選擇把很多數(shù)據(jù)直接寫入到堆外內(nèi)存里去,這樣的話,就不會占用 JVM 堆中的老年代空間了,也就不會導(dǎo)致老年代頻繁塞滿,頻繁觸發(fā) Full GC,導(dǎo)致系統(tǒng)性能頻繁抖動了。
如下圖:
那既然這個堆外內(nèi)存這么好,問題來了,他有什么缺點呢?
當(dāng)然有了,因為如果你用的是 JVM 堆內(nèi)的內(nèi)存,你寫入了很多數(shù)據(jù)以后,如果內(nèi)存滿了,此時 JVM 會自動進(jìn)行垃圾回收,幫你釋放掉一些內(nèi)存空間,他是全自動的。
但是如果你用的是堆外內(nèi)存,那可沒有 JVM 來幫你管理了,此時你必須自己管理那塊內(nèi)存空間。
也就是說,你寫入了數(shù)據(jù)以后,到了需要的時候,你得自己注意把部分內(nèi)存進(jìn)行釋放,所以這就導(dǎo)致了堆外內(nèi)存雖然不會導(dǎo)致你的 JVM 頻繁 GC,但是他可能會導(dǎo)致你的代碼管理難度變高。
如下圖:
那么這個堆外內(nèi)存一般來說我們用 Java 代碼是如何申請的呢?
看下面的代碼,一般類似 Netty、RocketMQ 等中間件因為就是要管理大量的內(nèi)存數(shù)據(jù),所以都會選擇申請一塊堆外內(nèi)存,把數(shù)據(jù)放在里面,自己進(jìn)行精細(xì)化的管理。
// 定義好要申請的堆外內(nèi)存的大小,這里是1GB
int memorySize = 1024 * 1024 * 1024;
// 用Java里的ByteBuffer.allocateDirect方法就可以申請一塊堆外內(nèi)存
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(memorySize);
// 把數(shù)據(jù)寫入到堆外內(nèi)存里去
byte[] bytes = "hello world".getBytes();
byteBuffer.put(bytes);
// 從堆外內(nèi)存里讀取數(shù)據(jù)
byteBuffer.flip();
byte[] readBytes = new byte[bytes.length];
byteBuffer.get(readBytes, 0, bytes.length);
那大家通過這塊代碼看到了我們?nèi)绾紊暾埗淹鈨?nèi)存,以及如何往堆外內(nèi)存里寫入數(shù)據(jù)和如何讀取數(shù)據(jù)之后,現(xiàn)在思考一下,堆外內(nèi)存我們應(yīng)該如何進(jìn)行釋放呢?
是這樣的,這個堆外內(nèi)存其實是被 JVM 堆內(nèi)的一個 ByteBuffer 對象來引用的,所以如果要是 JVM 堆內(nèi)的 ByteBuffer 對象被回收了,那他關(guān)聯(lián)的堆外內(nèi)存就會被釋放了。
如下圖:
好了,今天的知識點就分享到這里了,相信大家看完之后應(yīng)該對堆外內(nèi)存這個概念有了一個較為清晰的認(rèn)識了。