作者:京東零售 劉樂
上一篇文章說到JVM垃圾回收算法的兩個優(yōu)化標(biāo)的:吞吐量和停頓時長,并提到這兩個優(yōu)化目標(biāo)是有沖突的。那么有沒有可能提高吞吐量而不影響停頓時長,甚至縮短停頓時長呢?答案是有可能的,提高內(nèi)存占用(Memory Footprint)就有可能同時優(yōu)化這兩個標(biāo)的,這篇文章就來聊聊內(nèi)存相關(guān)內(nèi)容。
內(nèi)存占用一般指應(yīng)用運行需要的所有內(nèi)存,包括堆內(nèi)內(nèi)存(On-heap Memory)和堆外內(nèi)存(Off-heap Memory)
1. 堆內(nèi)內(nèi)存
堆內(nèi)內(nèi)存是分配給JVM的部分內(nèi)存,用來存放所有Java Class對象實例和數(shù)組,JVM GC操作的就是這部分內(nèi)容。我們先來回顧一下堆內(nèi)內(nèi)存的模型:
堆內(nèi)內(nèi)存包括年輕代(淺綠色),老年代(淺藍色),在JDK7或者更老的版本,圖中右邊還有個永久代(永久代在邏輯上位于JVM的堆區(qū),但又被稱為非堆內(nèi)存,在JDK8中被元空間取代)。JVM有動態(tài)調(diào)整內(nèi)存策略,通過-Xms,-Xmx 指定堆內(nèi)內(nèi)存動態(tài)調(diào)整的上下限。 在JVM初始化時實際只分配部分內(nèi)存,可通過-XX:InitialHeapSize指定初始堆內(nèi)存大小,未被分配的空間為圖中virtual部分。年輕代和老年代在每次GC的時候都有可能調(diào)整大小,以保證存活對象占用百分比在特定閾值范圍內(nèi),直到達到Xms指定的下限或Xms指定的上限。(閾值范圍通過-XX:MinHeapFreeRatio, XX:MaxHeapFreeRatio指定,默認值分別為40, 70)。
GC調(diào)優(yōu)中還有個的重要參數(shù)是老年代和年輕代的比例,通過-XX:NewRatio設(shè)定,與此相關(guān)的還有-XX:MaxNewSize和-XX:NewSize,分別設(shè)定年輕代大小的上下限,-Xmn則直接指定年輕代的大小。
1.1 參數(shù)默認值
?-Xmx: Xmx的默認值比較復(fù)雜,官方文檔上有時候?qū)懙氖?GB,但實際值跟JRE版本、JVM 模式(client, server)和系統(tǒng)(平臺類型,32位,64位)等都有關(guān)。經(jīng)過查閱源碼和實驗,確定在生產(chǎn)環(huán)境下(server模式,64位Centos,JRE 8),Xmx的默認值可以采用以下規(guī)則計算:
?容器內(nèi)存小于等于2G:默認值為容器內(nèi)存的1/2,最小16MB, 最大512MB。
?容器內(nèi)存大于2G:默認值為容器內(nèi)存的1/4, 最大可到達32G。
?-Xms: 默認值為容器內(nèi)存的1/64, 最小8MB,如果明確指定了Xmx并且小于容器內(nèi)存1/64, Xms默認值為Xmx指定的值。
?-NewRatio: 默認2,即年輕代和年老代的比例為1:2, 年輕代大小為堆內(nèi)內(nèi)存的1/3。
NOTE:在JRE版本1.8.0_131之前,JVM無法感知Docker的資源限制,Xmx, Xms未明確指定時,會使用宿主機的內(nèi)存計算默認值。
1.2 最佳實踐
由于每次Eden區(qū)滿就會觸發(fā)YGC,而每次YGC的時候,晉升到老年代的對象大小超過老年代剩余空間的時候,就會觸發(fā)FGC。所以基本來說,GC頻率和堆內(nèi)內(nèi)存大小是成反比的,也就是說堆內(nèi)內(nèi)存越大,吞吐量越大。
如果Xmx設(shè)置過小,不僅浪費了容器資源,在大流量下會頻繁GC,導(dǎo)致一系列問題,包括吞吐量降低,響應(yīng)變長,CPU升高,java.lang.OutOfMemoryError異常等。當(dāng)然Xmx也不建議設(shè)置過大,否則會導(dǎo)致進程hang住或者使用容器Swap。所以合理設(shè)置Xmx非常重要,特別是對于1.8.0_131之前的版本,一定要明確指定Xmx。推薦設(shè)置為容器內(nèi)存的50%,不能超過容器內(nèi)存的80%。
JVM的動態(tài)內(nèi)存策略不太適合服務(wù)使用,因為每次GC需要計算Heap是否需要伸縮,內(nèi)存抖動需要向系統(tǒng)申請或釋放內(nèi)存,特別是在服務(wù)重啟的預(yù)熱階段,內(nèi)存抖動會比較頻繁。另外,容器中如果有其他進程還在消費內(nèi)存,JVM內(nèi)存抖動時可能申請內(nèi)存失敗,導(dǎo)致OOM。因此建議服務(wù)模式下,將Xms設(shè)置Xmx一樣的值。
NewRatio建議在2~3之間,最優(yōu)選擇取決于對象的生命周期分布。一般先確定老年代的空間(足夠放下所有l(wèi)ive data,并適當(dāng)增加10%~20%),其余是年輕代,年輕代大小一定要小于老年代。
另外,以上建議都是基于一個容器部署一個JVM實例的使用情況。有個別需求,需要在一個容器內(nèi)啟用多個JVM,或者包含其他語言的,研發(fā)需要按業(yè)務(wù)需求在推薦值范圍內(nèi)分配JVM的Xmx。
2. 堆外內(nèi)存
和堆內(nèi)內(nèi)存對應(yīng)的就是堆外內(nèi)存。堆外內(nèi)存包括很多部分,比如Code Cache, Memory Pool,Stack Memory,Direct Byte Buffers, Metaspace等等,其中我們需要重點關(guān)注的是Direct Byte Buffers和Metaspace。
2.1 Direct Byte Buffers
Direct Byte Buffers是系統(tǒng)原生內(nèi)存,不位于JVM里,狹義上的堆外內(nèi)存就是指的Direct Byte Buffers。為什么要使用系統(tǒng)原生內(nèi)存呢? 為了更高效的進行Socket I/O或文件讀寫等內(nèi)核態(tài)資源操作,會使用JNI(Java原生接口),此時操作的內(nèi)存需要是連續(xù)和確定的。而Heap中的內(nèi)存不能保證連續(xù),且GC也可能導(dǎo)致對象隨時移動。因此涉及Output操作時,不直接使用Heap上的數(shù)據(jù),需要先從Heap上拷貝到原生內(nèi)存,Input操作則相反。因此為了避免多余的拷貝,提高I/O效率,不少第三方包和框架使用Direct Byte Buffers,比Netty。
Direct Byte Buffers雖然有上述優(yōu)點,但使用起來也有一定風(fēng)險。常見的Direct Byte Buffers使用方法是用java.nio.DirectByteBuffer的unsafe.allocateMemory方法來創(chuàng)建,DirectByteBuffer對象只保存了系統(tǒng)分配的原生內(nèi)存的大小和啟始位置,這些原生內(nèi)存的釋放需要等到DirectByteBuffer對象被回收。有些特殊的情況下(比如JVM一直沒有FGC,設(shè)置-XX:+DisableExplicitGC禁用了System.gc),這部分對象會持續(xù)增加,直到堆外內(nèi)存達到-XX:MaxDirectMemorySize 指定的大小或者耗盡所有的系統(tǒng)內(nèi)存。
MaxDirectMemorySize不明確指定的時候,默認值為0,在代碼中實際為Runtime.getRuntime().maxMemory(),略小于-Xmx指定的值(堆內(nèi)內(nèi)存的最大值減去一個Survivor區(qū)大小)。此默認值有點過大,MaxDirectMemorySize未設(shè)置或設(shè)置過大,有可能發(fā)生堆外內(nèi)存泄露,導(dǎo)致進程被系統(tǒng)Kill。
由于存在一定風(fēng)險,建議在啟動參數(shù)里明確指定-XX:MaxDirectMemorySize的值,并滿足下面規(guī)則:
?Xmx * 110% + MaxDirectMemorySize + 系統(tǒng)預(yù)留內(nèi)存 <= 容器內(nèi)存
?Xmx * 110% 中額外的10%是留給其他堆外內(nèi)存的,是個保守估計,個別業(yè)務(wù)運行時線程較多,需自行判斷,上式中左側(cè)還需加上Xss * 線程數(shù)
?系統(tǒng)預(yù)留內(nèi)存512M到1G,視容器規(guī)格而定
?I/O較多的業(yè)務(wù)適當(dāng)提高MaxDirectMemorySize比例
2.2 Metaspace
Metaspace(元空間)是JDK8關(guān)于方法區(qū)新的實現(xiàn),取代之前的永久代,用來保存類、方法、數(shù)據(jù)結(jié)構(gòu)等運行時信息和元信息的。很多研發(fā)在老版本時可能遇到過java.lang.OutOfMemoryError: PermGen Space,這說明永久代的空間不夠用了,JDK7以前可以通過-XX:PermSize,-XX:MaxPermSize來指定永久代的初始大小和最大大小。JDK8中Metaspace取代永久代,位置由JVM內(nèi)存變成系統(tǒng)原生內(nèi)存,也取消默認的最大空間限制。與此有關(guān)的參數(shù)主要有下面兩個:
?-XX:MaxMetaspaceSize 指定元空間的最大空間,默認為容器剩余的所有空間
?-XX:MetaspaceSize 指定元空間首次擴充的大小,默認為20.8M
由于MaxMetaspaceSize未指定時,默認無上限,所以需要特別關(guān)注內(nèi)存泄露的問題,如果程序動態(tài)的創(chuàng)建了很多類,或出現(xiàn)過java.lang.OutOfMemoryError:Metaspace,建議明確指定-XX:MaxMetaspaceSize。另外Metaspace實際分配的大小是隨著需要逐步擴大的,每次擴大需要一次FGC,-XX:MetaspaceSize默認的值比較小,需要頻繁GC擴充到需要的大小。通過下面的日志可以看到Metaspace引起的FGC:
[Full GC (Metadata GC Threshold) ...]
為減少預(yù)熱影響,可以將-XX:MetaspaceSize,-XX:MaxMetaspaceSize指定成相同的值。另外不少應(yīng)用由JDK7升級到了JDK8,但是啟動參數(shù)中仍有-XX:PermSize,-XX:MaxPermSize,這些參數(shù)是不生效的,建議修改成-XX:MetaspaceSize,-XX:MaxMetaspaceSize。
3. 應(yīng)用健康度檢查規(guī)則
?泰山應(yīng)用健康度現(xiàn)在已支持掃描JVM相關(guān)風(fēng)險,在應(yīng)用TAB的JVM配置檢測項下。主要包括以下檢測:
檢測指標(biāo) | 風(fēng)險等級 | 巡檢規(guī)則 |
---|---|---|
JVM版本 | 中危 | 版本不低于1.8.0_191 |
JVM GC方法 | 中危 | 所有分組GC方法一致 |
Xmx | 高危 | 明確指定,并且在容器內(nèi)存的50%~80%范圍內(nèi) |
Xms | 中危 | 明確指定,并且等于Xmx指定的值 |
堆外內(nèi)存 | 中危 | 明確指定,并且 堆內(nèi)*1.1+堆外+系統(tǒng)預(yù)留<=容器內(nèi)存 |
ParallelGCThreads | 高危 | ParallelGCThreads在容器CPU核數(shù)的50%~100%范圍內(nèi) |
ConcGCThreads | 低危 | ConcGCThreads在ParallelGCThreads的20%~50%范圍內(nèi)(限CMS,G1) |
CICompilerCount | 低危 | 指定CICompilerCount在推薦值50%~150%內(nèi)(限1.8 |
?上一篇文章已經(jīng)說了ParallelGCThreads,這里再補充一下新支持的兩個檢測,ConcGCThreads,CICompilerCount。
ConcGCThreads一般稱為并發(fā)標(biāo)記線程數(shù),為了減少GC的STW的時間,CMS和G1都有并發(fā)標(biāo)記的過程,此時業(yè)務(wù)線程仍在工作,只是并發(fā)標(biāo)記是CPU密集型任務(wù),業(yè)務(wù)的吞吐量會下降,RT會變長。ConcGCThreads的默認值不同GC策略略有不同,CMS下是(ParallelGCThreads + 3) / 4 向下取整,G1下是ParallelGCThreads / 4 四舍五入。一般來說采用默認值就可以了,但是還是由于在JRE版本1.8.0_131之前,JVM無法感知Docker的資源限制的問題,ConcGCThreads的默認值會比較大(20左右),對業(yè)務(wù)會有影響。
CICompilerCount是JIT進行熱點編譯的線程數(shù),和并發(fā)標(biāo)記線程數(shù)一樣,熱點編譯也是CPU密集型任務(wù),默認值為2。在CICompilerCountPerCPU開啟的時候(JDK7默認關(guān)閉,JDK8默認開啟),手動指定CICompilerCount是不會生效的,JVM會使用系統(tǒng)CPU核數(shù)進行計算。所以當(dāng)使用JRE8并且版本小于1.8.0_131,采用默認參數(shù)時,CICompilerCount會在20左右,對業(yè)務(wù)性能影響較大,特別是啟動階段。建議升級Java版本,特殊情況要使用老版本Java 8,請加上-XX:CICompilerCount=[n], 同時不能指定-XX:+CICompilerCountPerCPU ,下表給出了生產(chǎn)環(huán)境下常見規(guī)格的推薦值。
容器CPU核數(shù) | 1 | 2 | 4 | 8 | 16 |
---|---|---|---|---|---|
CICompilerCount手動指定推薦值 | 2 | 2 | 3 | 3 | 8 |
4. 修改建議
1) 再次建議升級JRE版本到1.8.0_191及以上; 2) 建議在Shell腳本中,Export JAVA_OPTS環(huán)境變量, 至少包含以下幾項(方括號中的值根據(jù)文中推薦選取):
-server -Xms[8192m] -Xmx[8192m] -XX:MaxDirectMemorySize=[4096m]
如果特殊原因要使用1.8.0_131以下版本, 則同時需要加上以下參數(shù)(方括號中的值根據(jù)文中推薦選取):
-XX:ParallelGCThreads=[8] -XX:ConcGCThreads=[2] -XX:CICompilerCount=[2]
下面的項建議測試后使用,需自行確定具體大小(特別是使用JRE8但仍配置-XX:PermSize,-XX:MaxPermSize的應(yīng)用):
-XX:MaxMetaspaceSize=[256]m -XX:MetaspaceSize=[256]m
環(huán)境變量設(shè)置如下例子:
export JAVA_OPTS="-Djava.library.path=/usr/local/lib -server -Xms4096m -Xmx4096m -XX:MaxMetaspaceSize=512m -XX:MetaspaceSize=512m -XX:MaxDirectMemorySize=2048m -XX:ParallelGCThreads=8 -XX:ConcGCThreads=2 -XX:CICompilerCount=2 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/export/Logs -XX:+UseG1GC [other_options...] -jar jarfile [args...]"
另外,如果應(yīng)用未接入UMP或PFinder, JAVA_OPTS中盡量不要用Shell函數(shù)或者變量,否則健康度有可能會提示解析失敗。
NOTE: Java options 的使用應(yīng)該按照下面的順序:
?執(zhí)行類: java [-options] class [args...]
?執(zhí)行包:java [-options] -jar jarfile [args...] 或 java -jar [-options] jarfile [args...]
即options要放到執(zhí)行對象之前,部分應(yīng)用使用了以下順序:
java -jar jarfile [-options] [args...] 或者 java -jar jarfile [args...] [-options]
這些Java options都不會生效。
修改補充說明:
上面的JVM參數(shù)配置已經(jīng)集成到行云部署了,在行云部署-分組-配置管理-JVM參數(shù)配置,點填充就可以配置默認的JVM參數(shù),如有需要也可以對特定的參數(shù)進行修改,注意worker類型應(yīng)用需要在啟動腳本(start.sh)中增加如下行:
source /home/admin/default_vm.sh
審核編輯 黃宇
-
內(nèi)存
+關(guān)注
關(guān)注
8文章
3055瀏覽量
74336 -
JVM
+關(guān)注
關(guān)注
0文章
158瀏覽量
12261 -
xmxl
+關(guān)注
關(guān)注
0文章
2瀏覽量
1247
發(fā)布評論請先 登錄
相關(guān)推薦
容器JVM內(nèi)存配置最佳實踐
![容器<b class='flag-5'>JVM</b><b class='flag-5'>內(nèi)存</b>配置最佳實踐](https://file1.elecfans.com/web2/M00/8A/1B/wKgaomSRBPKAJVjCAAAIMCFqdaI522.png)
評論