老闆讓我寫個 BUG!

前言

標題沒有看錯,真的是讓我寫個 bug!

剛接到這個需求時我內心沒有絲毫波瀾,甚至還有點激動。這可是我特長啊;終於可以光明正大的寫 bug 了。

先來看看具體是要幹啥吧,其實主要就是要讓一些負載很低的服務器額外消耗一些內存、CPU 等資源(至於背景就不多說了),讓它的負載可以提高一些。

JVM 內存分配回顧

於是我刷刷一把梭的就把代碼寫好了,大概如下:

老闆讓我寫個 BUG!

寫完之後我就在想一個問題,代碼中的 mem 對象在方法執行完之後會不會被立即回收呢?我想肯定會有一部分人認為就是在方法執行完之後回收。

我也正兒八經的去調研了下,問了一些朋友;果不其然確實有一部分認為是在方法執行完畢之後回收。

那事實情況如何呢?我做了一個試驗。

我用以下的啟動參數將剛才這個應用啟動起來。

<code>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/<code>

這樣我就可以通過 JMX 端口遠程連接到這個應用觀察內存、GC 情況了。

老闆讓我寫個 BUG!

如果是方法執行完畢就回收 mem 對象,當我分配 250M 內存時;內存就會有一個明顯的曲線,同時 GC 也會執行。

老闆讓我寫個 BUG!

這時觀察內存曲線。

老闆讓我寫個 BUG!

會發現確實有明顯的漲幅,但是之後並沒有立即回收,而是一直保持在這個水位。同時左邊的 GC 也沒有任何的反應。

用 jstat 查看內存佈局也是同樣的情況。

老闆讓我寫個 BUG!

不管是 YGC,FGC 都沒有,只是 Eden 區的使用佔比有所增加,畢竟分配了 250M 內存嘛。

那怎樣才會回收呢?

我再次分配了兩個 250M 之後觀察內存曲線。

老闆讓我寫個 BUG!

老闆讓我寫個 BUG!

發現第三個 250M 的時候 Eden 區達到了 98.83% 於是再次分配時就需要回收 Eden 區產生了 YGC。

同時內存曲線也得到了下降。

整個的換算過程如圖:

老闆讓我寫個 BUG!

由於初始化的堆內存為 4G,所以算出來的 Eden 區大概為 1092M 內存。

加上應用啟動 Spring 之類消耗的大約 20% 內存,所以分配 3 次 250M 內存就會導致 YGC。

再來回顧下剛才的問題:

老闆讓我寫個 BUG!

mem 對象既然在方法執行完畢後不會回收,那什麼時候回收呢。

其實只要記住一點即可:對象都需要垃圾回收器發生 GC 時才能回收;不管這個對象是局部變量還是全局變量。

通過剛才的實驗也發現了,當 Eden 區空間不足產生 YGC 時才會回收掉我們創建的 mem 對象。

但這裡其實還有一個隱藏條件:那就是這個對象是局部變量。如果該對象是全局變量那依然不能被回收。

也就是我們常說的對象不可達,這樣不可達的對象在 GC 發生時就會被認為是需要回收的對象從而進行回收。

在多考慮下,為什麼有些人會認為方法執行完畢後局部變量會被回收呢?

我想這應當是記混了,其實方法執行完畢後回收的是棧幀。

它最直接的結果就是導致 mem 這個對象沒有被引用了。但沒有引用並不代表會被馬上回收,也就是上面說到的需要產生 GC 才會回收。

所以使用的是上面提到的對象不可達所採用的可達性分析算法來表明哪些對象需要被回收。

當對象沒有被引用後也就認為不可達了。

這裡有一張動圖比較清晰:

老闆讓我寫個 BUG!

當方法執行完之後其中的 mem 對象就相當於圖中的 Object 5,所以在 GC 時候就會回收掉。

優先在 Eden 區分配對象

其實從上面的例子中可以看出對象是優先分配在新生代中 Eden 區的,但有個前提就是對象不能太大。

以前也寫過相關的內容:

老闆讓我寫個 BUG!

大對象直接進入老年代

而大對象則是直接分配到老年代中(至於多大算大,可以通過參數配置)。

老闆讓我寫個 BUG!

當我直接分配 1000M 內存時,由於 Eden 區不能直接裝下,所以改為分配在老年代中。

老闆讓我寫個 BUG!

老闆讓我寫個 BUG!

可以看到 Eden 區幾乎沒有變動,但是老年代卻漲了 37% ,根據之前計算的老年代內存 2730M 算出來也差不多是 1000M 的內存。

Linux 內存查看

回到這次我需要完成的需求:增加服務器內存和 CPU 的消耗。

CPU 還好,本身就有一定的使用,同時每創建一個對象也會消耗一些 CPU。

老闆讓我寫個 BUG!

主要是內存,先來看下沒啟動這個應用之前的內存情況。

老闆讓我寫個 BUG!

大概只使用了 3G 的內存。

啟動應用之後大概只消耗了 600M 左右的內存。

老闆讓我寫個 BUG!

為了滿足需求我需要分配一些內存,但這裡有點需要講究。

不能一直分配內存,這樣會導致 CPU 負載太高了,同時內存也會由於 GC 回收導致佔用也不是特別多。

所以我需要少量的分配,讓大多數對象在新生代中,為了不被回收需要保持在百分之八九十。

同時也需要分配一些大對象到老年代中,也要保持老年代的使用在百分之八九十。

這樣才能最大限度的利用這 4G 的堆內存。

於是我做了以下操作:

  • 先分配一些小對象在新生代中(800M)保持新生代在90%
  • 接著又分配了老年代內 *(100%-已使用的28%);也就是 2730*60%=1638M 讓老年代也在 90% 左右。
老闆讓我寫個 BUG!

老闆讓我寫個 BUG!

效果如上。

最主要的是一次 GC 都沒有發生這樣也就達到了我的目的。

最終內存消耗了 3.5G 左右。

老闆讓我寫個 BUG!

總結

雖說這次的需求是比較奇葩,但想要精確的控制 JVM 的內存分配還是沒那麼容易。

需要對它的內存佈局,回收都要有一定的瞭解,寫這個 Bug 的過程確實也加深了印象,如果對你有所幫助請不要吝嗇你的點贊與分享。


分享到:


相關文章: