前言
標題沒有看錯,真的是讓我寫個 bug
!
剛接到這個需求時我內心沒有絲毫波瀾,甚至還有點激動。這可是我特長啊;終於可以光明正大的寫 bug
了🙄。
先來看看具體是要干啥吧,其實主要就是要讓一些負載很低的服務器額外消耗一些內存、CPU 等資源(至於背景就不多說了),讓它的負載可以提高一些。
JVM 內存分配回顧
於是我刷刷一把梭的就把代碼寫好了,大概如下:
寫完之后我就在想一個問題,代碼中的 mem
對象在方法執行完之后會不會被立即回收呢?我想肯定會有一部分人認為就是在方法執行完之后回收。
我也正兒八經的去調研了下,問了一些朋友;果不其然確實有一部分認為是在方法執行完畢之后回收。
那事實情況如何呢?我做了一個試驗。
我用以下的啟動參數將剛才這個應用啟動起來。
java -Djava.rmi.server.hostname=10.xx.xx.xx
-Djava.security.policy=jstatd.all.policy
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.port=8888
-Xms4g -Xmx4g -jar bug-0.0.1-SNAPSHOT.jar
這樣我就可以通過 JMX 端口遠程連接到這個應用觀察內存、GC 情況了。
如果是方法執行完畢就回收 mem
對象,當我分配 250M
內存時;內存就會有一個明顯的曲線,同時 GC 也會執行。
這時觀察內存曲線。
會發現確實有明顯的漲幅,但是之后並沒有立即回收,而是一直保持在這個水位。同時左邊的 GC 也沒有任何的反應。
用 jstat
查看內存布局也是同樣的情況。
不管是 YGC,FGC
都沒有,只是 Eden 區的使用占比有所增加,畢竟分配了 250M 內存嘛。
那怎樣才會回收呢?
我再次分配了兩個 250M 之后觀察內存曲線。
發現第三個 250M 的時候 Eden
區達到了 98.83%
於是再次分配時就需要回收 Eden
區產生了 YGC
。
同時內存曲線也得到了下降。
整個的換算過程如圖:
由於初始化的堆內存為 4G
,所以算出來的 Eden
區大概為 1092M
內存。
加上應用啟動 Spring
之類消耗的大約 20%
內存,所以分配 3 次 250M 內存就會導致 YGC
。
再來回顧下剛才的問題:
mem
對象既然在方法執行完畢后不會回收,那什么時候回收呢。
其實只要記住一點即可:對象都需要垃圾回收器發生 GC
時才能回收;不管這個對象是局部變量還是全局變量。
通過剛才的實驗也發現了,當 Eden
區空間不足產生 YGC
時才會回收掉我們創建的 mem
對象。
但這里其實還有一個隱藏條件:那就是這個對象是局部變量。如果該對象是全局變量那依然不能被回收。
也就是我們常說的對象不可達,這樣不可達的對象在 GC
發生時就會被認為是需要回收的對象從而進行回收。
在多考慮下,為什么有些人會認為方法執行完畢后局部變量會被回收呢?
我想這應當是記混了,其實方法執行完畢后回收的是棧幀
。
它最直接的結果就是導致 mem
這個對象沒有被引用了。但沒有引用並不代表會被馬上回收,也就是上面說到的需要產生 GC
才會回收。
所以使用的是上面提到的對象不可達所采用的可達性分析算法來表明哪些對象需要被回收。
當對象沒有被引用后也就認為不可達了。
這里有一張動圖比較清晰:
當方法執行完之后其中的 mem
對象就相當於圖中的 Object 5
,所以在 GC
時候就會回收掉。
優先在 Eden 區分配對象
其實從上面的例子中可以看出對象是優先分配在新生代中 Eden 區的,但有個前提就是對象不能太大。
以前也寫過相關的內容:
大對象直接進入老年代
而大對象則是直接分配到老年代中(至於多大算大,可以通過參數配置)。
當我直接分配 1000M 內存時,由於 Eden 區不能直接裝下,所以改為分配在老年代中。
可以看到 Eden
區幾乎沒有變動,但是老年代卻漲了 37% ,根據之前計算的老年代內存 2730M
算出來也差不多是 1000M
的內存。
Linux 內存查看
回到這次我需要完成的需求:增加服務器內存和 CPU 的消耗。
CPU 還好,本身就有一定的使用,同時每創建一個對象也會消耗一些 CPU。
主要是內存,先來看下沒啟動這個應用之前的內存情況。
大概只使用了 3G 的內存。
啟動應用之后大概只消耗了 600M 左右的內存。
為了滿足需求我需要分配一些內存,但這里有點需要講究。
不能一直分配內存,這樣會導致 CPU 負載太高了,同時內存也會由於 GC 回收導致占用也不是特別多。
所以我需要少量的分配,讓大多數對象在新生代中,為了不被回收需要保持在百分之八九十。
同時也需要分配一些大對象到老年代中,也要保持老年代的使用在百分之八九十。
這樣才能最大限度的利用這 4G 的堆內存。
於是我做了以下操作:
- 先分配一些小對象在新生代中(800M)保持新生代在90%
- 接着又分配了
老年代內 *(100%-已使用的28%);也就是 2730*60%=1638M
讓老年代也在 90% 左右。
效果如上。
最主要的是一次 GC
都沒有發生這樣也就達到了我的目的。
最終內存消耗了 3.5G 左右。
總結
雖說這次的需求是比較奇葩,但想要精確的控制 JVM
的內存分配還是沒那么容易。
需要對它的內存布局,回收都要有一定的了解,寫這個 Bug 的過程確實也加深了印象,如果對你有所幫助請不要吝嗇你的點贊與分享。
你的點贊與分享是對我最大的支持