開發(fā) | 老板讓我寫個(gè)Bug,這可咋整?
事情是這個(gè)樣子的,作為兢兢業(yè)業(yè)、勤勤懇懇的小碼農(nóng),雖無法做到沉迷代碼不可自拔的地步。但是!我們早已練就一身捕獲 Bug 的技能,鏟除程序 Bug 已經(jīng)成為人生宗旨,并且,打算就此長久保持。
本以為能安安穩(wěn)穩(wěn)、快快樂樂做碼農(nóng),老板的一句話,如雷貫耳,擊碎了我的小初心,老板讓我寫個(gè) Bug,這可咋整?
標(biāo)題沒有看錯(cuò),真的是讓我寫個(gè) Bug!剛接到這個(gè)需求時(shí)我內(nèi)心沒有絲毫波瀾,甚至還有點(diǎn)激動(dòng)。這可是我特長啊,終于可以光明正大的寫 Bug 了。
先來看看具體是要干啥吧,其實(shí)主要就是要讓一些負(fù)載很低的服務(wù)器額外消耗一些內(nèi)存、CPU 等資源(至于背景就不多說了),讓它的負(fù)載可以提高一些。
JVM 內(nèi)存分配回顧
于是我刷刷一把梭的就把代碼寫好了,大概如下:
寫完之后我就在想一個(gè)問題,代碼中的 mem 對(duì)象在方法執(zhí)行完之后會(huì)不會(huì)被立即回收呢?我想肯定會(huì)有一部分人認(rèn)為就是在方法執(zhí)行完之后回收。
我也正兒八經(jīng)的去調(diào)研了下,問了一些朋友;果不其然確實(shí)有一部分認(rèn)為是在方法執(zhí)行完畢之后回收。
那事實(shí)情況如何呢?我做了一個(gè)試驗(yàn)。我用以下的啟動(dòng)參數(shù)將剛才這個(gè)應(yīng)用啟動(dòng)起來:
這樣我就可以通過 JMX 端口遠(yuǎn)程連接到這個(gè)應(yīng)用觀察內(nèi)存、GC 情況了。
如果是方法執(zhí)行完畢就回收 mem 對(duì)象,當(dāng)我分配 250M 內(nèi)存時(shí);內(nèi)存就會(huì)有一個(gè)明顯的曲線,同時(shí) GC 也會(huì)執(zhí)行。
這時(shí)觀察內(nèi)存曲線,如下圖:
你會(huì)發(fā)現(xiàn)確實(shí)有明顯的漲幅,但是之后并沒有立即回收,而是一直保持在這個(gè)水位。同時(shí)左邊的 GC 也沒有任何的反應(yīng)。
用 jstat 查看內(nèi)存布局也是同樣的情況,如下圖:
不管是 YGC,F(xiàn)GC 都沒有,只是 Eden 區(qū)的使用占比有所增加,畢竟分配了 250M 內(nèi)存嘛。
那怎樣才會(huì)回收呢?我再次分配了兩個(gè) 250M 之后觀察內(nèi)存曲線。
發(fā)現(xiàn)第三個(gè) 250M 的時(shí)候,Eden 區(qū)達(dá)到了 98.83%。于是再次分配時(shí)就需要回收 Eden 區(qū)產(chǎn)生了 YGC。同時(shí)內(nèi)存曲線也得到了下降。
整個(gè)的換算過程如下圖:
由于初始化的堆內(nèi)存為 4G,所以算出來的 Eden 區(qū)大概為 1092M 內(nèi)存。
加上應(yīng)用啟動(dòng) Spring 之類消耗的大約 20% 內(nèi)存,所以分配 3 次 250M 內(nèi)存就會(huì)導(dǎo)致 YGC。
再來回顧下剛才的問題:
mem 對(duì)象既然在方法執(zhí)行完畢后不會(huì)回收,那什么時(shí)候回收呢?
其實(shí)只要記住一點(diǎn)即可:對(duì)象都需要垃圾回收器發(fā)生 GC 時(shí)才能回收;不管這個(gè)對(duì)象是局部變量還是全局變量。
通過剛才的實(shí)驗(yàn)也發(fā)現(xiàn)了,當(dāng) Eden 區(qū)空間不足產(chǎn)生 YGC 時(shí)才會(huì)回收掉我們創(chuàng)建的 mem 對(duì)象。
但這里其實(shí)還有一個(gè)隱藏條件:那就是這個(gè)對(duì)象是局部變量。如果該對(duì)象是全局變量那依然不能被回收。
也就是我們常說的對(duì)象不可達(dá),這樣不可達(dá)的對(duì)象在 GC 發(fā)生時(shí)就會(huì)被認(rèn)為是需要回收的對(duì)象從而進(jìn)行回收。
在多考慮下,為什么有些人會(huì)認(rèn)為方法執(zhí)行完畢后局部變量會(huì)被回收呢?
我想這應(yīng)當(dāng)是記混了,其實(shí)方法執(zhí)行完畢后回收的是棧幀。它最直接的結(jié)果就是導(dǎo)致 mem 這個(gè)對(duì)象沒有被引用了。
但沒有引用并不代表會(huì)被馬上回收,也就是上面說到的需要產(chǎn)生 GC 才會(huì)回收。
所以使用的是上面提到的對(duì)象不可達(dá)所采用的可達(dá)性分析算法來表明哪些對(duì)象需要被回收。當(dāng)對(duì)象沒有被引用后也就認(rèn)為不可達(dá)了。
這里有一張動(dòng)圖比較清晰:
當(dāng)方法執(zhí)行完之后其中的 mem 對(duì)象就相當(dāng)于圖中的 Object5,所以在 GC 時(shí)候就會(huì)回收掉。
優(yōu)先在 Eden 區(qū)分配對(duì)象
從上面的例子中可以看出對(duì)象是優(yōu)先分配在新生代中 Eden 區(qū)的,但有個(gè)前提就是對(duì)象不能太大。
以前也寫過相關(guān)的內(nèi)容:

大對(duì)象直接進(jìn)入老年代
而大對(duì)象則是直接分配到老年代中(至于多大算大,可以通過參數(shù)配置)。
當(dāng)我直接分配 1000M 內(nèi)存時(shí),由于 Eden 區(qū)不能直接裝下,所以改為分配在老年代中。
可以看到 Eden 區(qū)幾乎沒有變動(dòng),但是老年代卻漲了 37% ,根據(jù)之前計(jì)算的老年代內(nèi)存 2730M 算出來也差不多是 1000M 的內(nèi)存。
Linux 內(nèi)存查看
回到這次我需要完成的需求:增加服務(wù)器內(nèi)存和 CPU 的消耗。
CPU 還好,本身就有一定的使用,同時(shí)每創(chuàng)建一個(gè)對(duì)象也會(huì)消耗一些 CPU。
主要是內(nèi)存,先來看下沒啟動(dòng)這個(gè)應(yīng)用之前的內(nèi)存情況:
大概只使用了 3G 的內(nèi)存。啟動(dòng)應(yīng)用之后大概只消耗了 600M 左右的內(nèi)存。
為了滿足需求我需要分配一些內(nèi)存,但這里有點(diǎn)需要講究。不能一直分配內(nèi)存,這樣會(huì)導(dǎo)致 CPU 負(fù)載太高了,同時(shí)內(nèi)存也會(huì)由于 GC 回收導(dǎo)致占用也不是特別多。
所以我需要少量的分配,讓大多數(shù)對(duì)象在新生代中,為了不被回收需要保持在百分之八九十。
同時(shí)也需要分配一些大對(duì)象到老年代中,也要保持老年代的使用在百分之八九十。這樣才能***限度的利用這 4G 的堆內(nèi)存。
于是我做了以下操作:
- 先分配一些小對(duì)象在新生代中(800M)保持新生代在 90%。
- 接著又分配了老年代內(nèi)*(100%-已使用的28%);也就是2730*60%=1638M 讓老年代也在 90% 左右。
效果如上。最主要的是一次 GC 都沒有發(fā)生這樣也就達(dá)到了我的目的。最終內(nèi)存消耗了 3.5G 左右。
總結(jié)
雖說這次的需求是比較奇葩,但想要精確的控制 JVM 的內(nèi)存分配還是沒那么容易。
需要對(duì)它的內(nèi)存布局,回收都要有一定的了解,寫這個(gè) Bug 的過程確實(shí)也加深了印象,如果對(duì)你有所幫助請(qǐng)不要吝嗇你的點(diǎn)贊與分享。