運維:大家 JAVA 服務內存佔用過高,還只增不減!告警了,快來接鍋

先點贊再看,養成好習慣java

某天,運維老哥忽然找我:「大家的某 JAVA 服務內存佔用過高,告警了!GC 後也沒釋放,內存只增不減,是否是內存泄漏了!」算法

而後我趕忙看了下監控,一切正常,距離上次發版好幾天了,FULL GC 一次沒有,YoungGC,十分鐘一次,堆空閒也很充足。服務器

運維:「大家這個服務如今堆內存 used 才 800M,但這個 JAVA 進程已經佔了 6G 內存了,是否是大家程序出啥內存泄露的 bug 了!」
我想都沒想,直接回了一句:「不可能,咱們服務很是穩定,不會有這種問題!」

image.png

不過說完以後,心裏仍是自我質疑了一下:會不會真有什麼bug?難道是堆外泄露?線程沒銷燬?致使內存泄露了???oracle

而後我很「鎮定」的補了一句:「我先上服務器看看啥狀況」,被打臉可就很差了,仍是不要裝太滿的好……

迅速上登上服務器又仔細的查看了各類指標,Heap/GC/Thread/Process 之類的,發現一切正常,並無什麼「泄漏」的跡象。運維

和運維的「溝通」

咱們這個服務很正常啊,各個指標都ok,什麼內存只增不減,在哪呢

image.png

運維:你看大家這個 JAVA 服務,堆如今 used 才 400MB,但這個進程如今內存佔用都 6G 了,還說沒問題?確定是內存泄露了,鍋接好,趕忙回去查問題吧 測試

而後我指着監控信息,讓運維看:「大哥你看這監控歷史,堆內存是達到過 6G 的,只是後面 GC 了,沒問題啊!」spa

運維:「回收了你這內存也沒釋放啊,你看這個進程 Res 仍是 6G,確定有問題啊」操作系統

我心想這運維怕不是個der,JVM GC 回收和進程內存又不是一回事,不過仍是和得他解釋一下,否則一直baba個沒完線程

「JVM 的垃圾回收,只是一個邏輯上的回收,回收的只是 JVM 申請的那一塊邏輯堆區域,將數據標記爲空閒之類的操做,不是調用 free 將內存歸還給操做系統」code

運維頓了兩秒後,忽然臉色一轉,開始笑起來:「咳咳,我可能沒注意這個。你再給我講講 JVM 的這個內存管理/回收和進程上內存的關係唄」

雖然我心裏是拒絕的,但得罪誰也不能得罪運維啊,想一想仍是給大哥解釋解釋,「增進下感情」

image.png

操做系統 與 JVM的內存分配

JVM 的自動內存管理,其實只是先向操做系統申請了一大塊內存,而後本身在這塊已申請的內存區域中進行「自動內存管理」。JAVA 中的對象在建立前,會先從這塊申請的一大塊內存中劃分出一部分來給這個對象使用,在 GC 時也只是這個對象所處的內存區域數據清空,標記爲空閒而已

運維:「原來是這樣,那按你的意思,JVM 就不會將 GC 回收後的空閒內存還給操做系統了嗎?」

爲何不把內存歸還給操做系統?

JVM 仍是會歸還內存給操做系統的,只是由於這個代價比較大,因此不會輕易進行。並且不一樣垃圾回收器 的內存分配算法不一樣,歸還內存的代價也不一樣。

好比在清除算法(sweep)中,是經過空閒鏈表(free-list)算法來分配內存的。簡單的說就是將已申請的大塊內存區域分爲 N 個小區域,將這些區域同鏈表的結構組織起來,就像這樣:

image.png

每一個 data 區域能夠容納 N 個對象,那麼當一次 GC 後,某些對象會被回收,但是此時這個 data 區域中還有其餘存活的對象,若是想將整個 data 區域釋放那是確定不行的。

因此這個歸還內存給操做系統的操做並無那麼簡單,執行起來代價太高,JVM 天然不會在每次 GC 後都進行內存的歸還。

怎麼歸還?

雖然代價高,但 JVM 仍是提供了這個歸還內存的功能。JVM 提供了-XX:MinHeapFreeRatio-XX:MaxHeapFreeRatio 兩個參數,用於配置這個歸還策略。

  • MinHeapFreeRatio 表明當空閒區域大小降低到該值時,會進行擴容,擴容的上限爲 Xmx
  • MaxHeapFreeRatio 表明當空閒區域超過該值時,會進行「縮容」,縮容的下限爲Xms

不過雖然有這個歸還的功能,不過由於這個代價比較昂貴,因此 JVM 在歸還的時候,是線性遞增歸還的,並非一次所有歸還。

可是可是可是,通過實測,這個歸還內存的機制,在不一樣的垃圾回收器,甚至不一樣的 JDK 版本中還不同!

不一樣版本&垃圾回收器下的表現不一樣

下面是我以前跑過的測試結果:

public static void main(String[] args) throws IOException, InterruptedException {
    List<Object> dataList = new ArrayList<>();
    for (int i = 0; i < 25; i++) {
        byte[] data = createData(1024 * 1024 * 40);// 40 MB
        dataList.add(data);
    }
    Thread.sleep(10000);
    dataList = null; // 待會 GC 直接回收
    for (int i = 0; i < 100; i++) {
        // 測試屢次 GC
        System.gc();
        Thread.sleep(1000);
    }
    System.in.read();
}
public static byte[] createData(int size){
    byte[] data = new byte[size];
    for (int i = 0; i < size; i++) {
        data[i] = Byte.MAX_VALUE;
    }
    return data;
}
JAVA 版本 垃圾回收器 VM Options 是否能夠「歸還」
JAVA 8 UseParallelGC(ParallerGC + ParallerOld) -Xms100M -Xmx2G -XX:MaxHeapFreeRatio=40
JAVA 8 CMS+ParNew -Xms100M -Xmx2G -XX:MaxHeapFreeRatio=40 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
JAVA 8 UseG1GC(G1) -Xms100M -Xmx2G -XX:MaxHeapFreeRatio=40 -XX:+UseG1GC
JAVA 11 UseG1GC(G1) -Xms100M -Xmx2G -XX:MaxHeapFreeRatio=40
JAVA 16 UseZGC(ZGC) -Xms100M -Xmx2G -XX:MaxHeapFreeRatio=40 -XX:+UseZGC

測試結果刷新了個人認知。,MaxHeapFreeRatio 這個參數好像並無什麼用,不管我是配置40,仍是配置90,回收的比例都有和實際的結果都有很大差距。

可是文檔中,可不是這麼說的……

並且 ZGC 的結果也是挺意外的,JEP 351 提到了 ZGC 會將未使用的內存釋放,但測試結果裏並無。

除了以上測試結果,stackoverflow 上還有一些其餘的說法,我就沒有再一一測試了

  1. JAVA 9 後-XX:-ShrinkHeapInSteps參數,可讓 JVM 已非線性遞增的方式歸還內存
  2. JAVA 12 後的 G1,再應用空閒時,能夠自動的歸還內存

因此,官方文檔的說法,也只能看成一個參考,JVM 並無過多的透露這個實現細節。

不過這個是否歸還的機制,除了這位「熱情」的運維老哥,通常人也不太會去關心,恨不得 JVM 多用點內存,少 GC 幾次……

並且別說空閒自動歸還了,咱們但願的是一啓動就分配個最大內存,避免它運行中擴容影響服務;因此通常 JAVA 程序還會將 XmsXmx配置爲相等的大小,避免這個擴容的操做。

聽到這裏,運維老哥如有所思的說到:「那是否是隻要我把 Xms 和 Xmx 配置成同樣的大小,這個 JAVA 進程一啓動就會佔用這個大小的內存呢?」
我接着答到:「不會的,哪怕你 Xms6G,啓動也只會佔用實際寫入的內存,大機率達不到 6G,這裏還涉及一個操做系統內存分配的小知識」

Xms6G,爲何啓動以後 used 才 200M?

進程在申請內存時,並非直接分配物理內存的,而是分配一塊虛擬空間,到真正堆這塊虛擬空間寫入數據時纔會經過缺頁異常(Page Fault)處理機制分配物理內存,也就是咱們看到的進程 Res 指標。

能夠簡單的認爲操做系統的內存分配是「惰性」的,分配並不會發生實際的佔用,有數據寫入時纔會發生內存佔用,影響 Res。

因此,哪怕配置了Xms6G,啓動後也不會直接佔用 6G 內存,只是 JVM 在啓動後會malloc 6G 而已,但實際佔用的內存取決於你有沒有往這 6G 內存區域中寫數據的。

運維:「臥槽,還有惰性分配這種東西!長知識了」

我:「這下明白了吧,這個內存狀況是正常的,咱們的服務一點問題都沒有」

運維:「🐂🍺,是我理解錯了,大家這個服務沒啥問題」

我:「嗯吶,沒事那我先去忙(摸魚)了」

image.png

總結

對於大多數服務端場景來講,並不須要JVM 這個手動釋放內存的操做。至於 JVM 是否歸還內存給操做系統這個問題,咱們也並不關心。並且基於上面那個測試結果,不一樣 JAVA 版本,不一樣垃圾回收器版本區別這麼大,更是不必去深究了。

綜上,JVM 雖然能夠釋放空閒內存給操做系統,可是不必定會釋放,在不一樣 JAVA 版本,不一樣垃圾回收器版本下表現不一樣,知道有這個機制就行。

參考

原創不易,禁止未受權的轉載。若是個人文章對您有幫助,就請點贊/收藏/關注鼓勵支持一下把!
相關文章
相關標籤/搜索