全球今頭條!【JVM故障問題排查心得】「內(nèi)存診斷系列」Xmx和Xms的大小是小于Docker容器以及Pod的大小的,為啥還是會出現(xiàn)OOMKilled?

2023-01-01 19:14:36 來源:51CTO博客

為什么我設(shè)置的大小關(guān)系沒有錯,還會OOMKilled?

這種問題常發(fā)生在JDK8u131或者JDK9版本之后所出現(xiàn)在容器中運(yùn)行JVM的問題:在大多數(shù)情況下,JVM將一般默認(rèn)會采用宿主機(jī)Node節(jié)點(diǎn)的內(nèi)存為Native VM空間(其中包含了堆空間、直接內(nèi)存空間以及棧空間),而并非是是容器的空間為標(biāo)準(zhǔn)。

堆內(nèi)存和VM實(shí)際分配內(nèi)存不一致

-XshowSettings:vm

Jps -lVvm

我們在運(yùn)行的時候?qū)VM堆內(nèi)存內(nèi)存設(shè)置為3000MB,而-XshowSettings:vm打印出的JVM將最大堆大小為1.09G,如果按照這個內(nèi)存進(jìn)行分配內(nèi)存的話很可能會導(dǎo)致實(shí)際內(nèi)存和預(yù)分配內(nèi)存所造成的不一致問題。


(資料圖片僅供參考)

如何解決此問題

JVM 感知 cgroup 限制

解決JVM內(nèi)存超限的問題,這種方法可以讓JVM自動感知Docker容器的cgroup限制,從而動態(tài)的調(diào)整堆內(nèi)存大小。

JDK8u131在JDK9中有一個很好的特性,即JVM能夠檢測在Docker容器中運(yùn)行時有多少內(nèi)存可用。為了使jvm保留根據(jù)容器規(guī)范的內(nèi)存,必須設(shè)置標(biāo)志??-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap??。

注意:如果將這兩個標(biāo)志與Xms和Xmx標(biāo)志一起設(shè)置,那么jvm的行為將是什么?-Xmx標(biāo)志將覆蓋-XX:+ UseCGroupMemoryLimitForHeap標(biāo)志

參數(shù)分析

??-XX:+ UseCGroupMemoryLimitForHeap??標(biāo)志使JVM可以檢測容器中的最大堆大小。??-Xmx??標(biāo)志將最大堆大小設(shè)置為固定大小。

除了JVM的堆空間,還會對于非堆Noheap和JVM的東西,還會有一些額外的內(nèi)存使用情況。

使用JDK9的容器感知機(jī)制嘗試

設(shè)置了容器有4GB內(nèi)存分配,而JVM使用1GM作為最大堆,因?yàn)槿萜髦谐薐VM之外沒有其他進(jìn)程在運(yùn)行,所以我們還可以進(jìn)一步擴(kuò)大一下對于Heap堆的分配?

-XX:MaxRAMFraction

在較低的版本的時候可以使用-XX:MaxRAMFraction參數(shù),它告訴JVM使用可用內(nèi)存/MaxRAMFract作為最大堆。使用-XX:MaxRAMFractinotallow=1,我們將幾乎所有可用內(nèi)存用作最大堆。

問題分析

最大堆占用總內(nèi)存是否仍然會導(dǎo)致你的進(jìn)程因?yàn)閮?nèi)存的其他部分(如“元空間”)而被殺死?

答案:MaxRAMFractinotallow=1仍將為其他非堆內(nèi)存留出一些空間

注意:如果容器使用堆外內(nèi)存,這可能會有風(fēng)險(xiǎn),因?yàn)閹缀跛械娜萜鲀?nèi)存都分配給了堆。您必須將-XX:MaxRAMFractinotallow=2設(shè)置為堆只使用50%的容器內(nèi)存,或者使用Xmx。

容器內(nèi)部感知CGroup資源限制

Docker1.7開始將容器cgroup信息掛載到容器中,所以應(yīng)用可以從 /sys/fs/cgroup/memory/memory.limit_in_bytes 等文件獲取內(nèi)存、 CPU等設(shè)置,在容器的應(yīng)用啟動命令中根據(jù)Cgroup配置正確的資源設(shè)置 ??-Xmx, -XX:ParallelGCThreads?? 等參數(shù)

Java10中,改進(jìn)了容器集成

Java10+廢除了-XX:MaxRAM參數(shù),因?yàn)镴VM將正確檢測該值。在Java10中,改進(jìn)了容器集成,無需添加額外的標(biāo)志,JVM將使用1/4的容器內(nèi)存用于堆。

java10+確實(shí)正確地識別了內(nèi)存的docker限制,但您可以使用新的標(biāo)志MaxRAMPercentage(例如:-XX:MaxRAMPercentage=75)而不是舊的MaxRAMFraction,以便更精確地調(diào)整堆的大小。

java10+上的UseContainerSupport選項(xiàng),而且是默認(rèn)啟用的,不用設(shè)置。同時 UseCGroupMemoryLimitForHeap 這個就棄用了,不建議繼續(xù)使用,同時還可以通過?? -XX:InitialRAMPercentage、-XX:MaxRAMPercentage、-XX:MinRAMPercentage?? 這些參數(shù)更加細(xì)膩的控制 JVM 使用的內(nèi)存比率。

-XX:MaxRAMFraction

Java 程序在運(yùn)行時會調(diào)用外部進(jìn)程、申請 Native Memory 等,所以即使是在容器中運(yùn)行 Java 程序,也得預(yù)留一些內(nèi)存給系統(tǒng)的。所以 -XX:MaxRAMPercentage 不能配置得太大。當(dāng)然仍然可以使用-XX:MaxRAMFractinotallow=1選項(xiàng)來壓縮容器中的所有內(nèi)存。

上面我們知道了如何進(jìn)行設(shè)置和控制對應(yīng)的堆內(nèi)存和容器內(nèi)存的之間的關(guān)系,所以防止JVM的堆內(nèi)存超過了容器內(nèi)存,導(dǎo)致容器出現(xiàn)OOMKilled的情況。但是在整個JVM進(jìn)程體系而言,不僅僅只包含了Heap堆內(nèi)存,其實(shí)還有其他相關(guān)的內(nèi)存存儲空間是需要我們考慮的,一邊防止這些內(nèi)存空間會造成我們的容器內(nèi)存溢出的場景。

Off Heap Space

接下來了我們需要進(jìn)行分析出heap之外的一部分就是對外內(nèi)存就是Off Heap Space,也就是Direct buffer memory堆外內(nèi)存。主要通過的方式就是采用Unsafe方式進(jìn)行申請內(nèi)存,大多數(shù)場景也會通過Direct ByteBuffer方式進(jìn)行獲取。好廢話不多說進(jìn)入正題。

JVM參數(shù)MaxDirectMemorySize

研究一下jvm的-XX:MaxDirectMemorySize,該參數(shù)指定了DirectByteBuffer能分配的空間的限額,如果沒有顯示指定這個參數(shù)啟動jvm,默認(rèn)值是xmx對應(yīng)的值(低版本是減去幸存區(qū)的大小)。

而Runtime.maxMemory()在HotSpot VM里的實(shí)現(xiàn)是:

-Xmx減去一個survivor space的預(yù)留大小

DirectByteBuffer對象是一種典型的”冰山對象”,在堆中存在少量的泄露的對象,但其下面連接用堆外內(nèi)存,這種情況容易造成內(nèi)存的大量使用而得不到釋放

-XX:MaxDirectMemorySize=size 用于設(shè)置 New I/O (java.nio) direct-buffer allocations 的最大大小,size 的單位可以使用 k/K、m/M、g/G;如果沒有設(shè)置該參數(shù)則默認(rèn)值為 0,意味著JVM自己自動給NIO direct-buffer allocations選擇最大大小。

-XX:MaxDirectMemorySize的默認(rèn)值是什么?
在sun.misc.VM中,它是??Runtime.getRuntime.maxMemory()??,這就是使用-Xmx配置的內(nèi)容。而對應(yīng)的JVM參數(shù)如何傳遞給JVM底層的呢?主要通過hotspot/share/prims/jvm.cpp。jvm.cpp里頭有一段代碼用于把 ??-XX:MaxDirectMemorySize??? 命令參數(shù)轉(zhuǎn)換為key為 sun.nio.MaxDirectMemorySize的屬性。我們可以看出來他轉(zhuǎn)換為了該屬性之后,進(jìn)行設(shè)置和初始化直接內(nèi)存的配置。針對于直接內(nèi)存的核心類就在, 在??-XX:MaxDirectMemorySize?? 是用來配置NIO direct memory上限用的VM參數(shù)。但如果不配置它的話,direct memory默認(rèn)最多能申請多少內(nèi)存呢?這個參數(shù)默認(rèn)值是-1,顯然不是一個“有效值”。

sun.nio.MaxDirectMemorySize 屬性,如果為 null 或者是空或者是 - 1,那么則設(shè)置為 Runtime.getRuntime ().maxMemory ();因?yàn)楫?dāng)MaxDirectMemorySize參數(shù)沒被顯式設(shè)置時它的值就是-1,在Java類庫初始化時maxDirectMemory()被java.lang.System的靜態(tài)構(gòu)造器調(diào)用。

這個max_capacity()實(shí)際返回的是 -Xmx減去一個survivor space的預(yù)留大小

結(jié)論分析說明

MaxDirectMemorySize沒顯式配置的時候,NIO direct memory可申請的空間的上限就是-Xmx減去一個survivor space的預(yù)留大小。例如如果您不配置-XX:MaxDirectMemorySize并配置-Xmx5g,則"默認(rèn)" MaxDirectMemorySize也將是5GB-survivor space區(qū),并且應(yīng)用程序的總堆+直接內(nèi)存使用量可能會增長到5 + 5 = 10 Gb 。

其他獲取 maxDirectMemory 的值的API方法

BufferPoolMXBean 及 JavaNioAccess.BufferPool (通過SharedSecrets獲取) 的 getMemoryUsed 可以獲取 direct memory 的大小;其中 java9 模塊化之后,SharedSecrets 從原來的 sun.misc.SharedSecrets 變更到 java.base 模塊下的 jdk.internal.access.SharedSecrets;要使用 --add-exports java.base/jdk.internal.access=ALL-UNNAMED 將其導(dǎo)出到 UNNAMED,這樣才可以運(yùn)行

內(nèi)存分析問題

-XX:+DisableExplicitGC 與 NIO的direct memory

用了-XX:+DisableExplicitGC參數(shù)后,System.gc()的調(diào)用就會變成一個空調(diào)用,完全不會觸發(fā)任何GC(但是“函數(shù)調(diào)用”本身的開銷還是存在的哦~)。

做ygc的時候會將新生代里的不可達(dá)的DirectByteBuffer對象及其堆外內(nèi)存回收了,但是無法對old里的DirectByteBuffer對象及其堆外內(nèi)存進(jìn)行回收,這也是我們通常碰到的最大的問題,如果有大量的DirectByteBuffer對象移到了old,但是又一直沒有做cms gc或者full gc,而只進(jìn)行ygc,那么我們的物理內(nèi)存可能被慢慢耗光,但是我們還不知道發(fā)生了什么,因?yàn)閔eap明明剩余的內(nèi)存還很多(前提是我們禁用了System.gc)。

標(biāo)簽: 方式進(jìn)行 可以使用

上一篇:實(shí)時焦點(diǎn):靜態(tài)路由配置
下一篇:OSI 二層技術(shù)之STP生成樹協(xié)議