Docker環(huán)境下SpringBoot內(nèi)存飆升與解決方案
SpringBoot應(yīng)用內(nèi)存飆升
一個(gè)簡(jiǎn)單的Spring Boot應(yīng)用,幾乎只有一個(gè)用戶在用,內(nèi)存竟然達(dá)到1.2G, 可怕

服務(wù)現(xiàn)狀
由于之前服務(wù)比較少,服務(wù)器資源充足,許多服務(wù)啟動(dòng)時(shí)都未添加JVM參數(shù)(遺留問(wèn)題)。
結(jié)果就是每個(gè)服務(wù)啟動(dòng)都占用了1.2G-2G的內(nèi)存,有些服務(wù)的體量根本用不了這么多。那么,在Spring Boot中如果未設(shè)置JVM內(nèi)存參數(shù)時(shí),JVM內(nèi)存是如何配置的呢?
JVM默認(rèn)內(nèi)存設(shè)置
當(dāng)運(yùn)行一個(gè)Spring Boot項(xiàng)目時(shí),如果未設(shè)置JVM內(nèi)存參數(shù),Spring Boot默認(rèn)會(huì)采用JVM自身默認(rèn)的配置策略。
在資源比較充足的情況下,開(kāi)發(fā)者倒是不太用關(guān)心內(nèi)存的設(shè)置。但一旦涉及到資源不足,JVM優(yōu)化,那么就需要了解默認(rèn)的JVM內(nèi)存配置策略。
關(guān)于JVM內(nèi)存最常見(jiàn)的設(shè)置為初始堆大?。?Xms)和最大堆內(nèi)存(-Xmx)。很多人懶得去設(shè)置,而是采用JVM的默認(rèn)值。特別是在開(kāi)發(fā)環(huán)境下,如果啟動(dòng)的微服務(wù)比較多,內(nèi)存會(huì)被撐爆。
而JVM默認(rèn)內(nèi)存配置策略分兩種場(chǎng)景,大內(nèi)存空間場(chǎng)景和小內(nèi)存空間場(chǎng)景(小于192M)。
以4GB內(nèi)存為例,初始堆內(nèi)存大小和最大堆內(nèi)存大小如下圖:

默認(rèn)情況下,最大堆內(nèi)存占用物理內(nèi)存的1/4,如果應(yīng)用程序超過(guò)該上限,則會(huì)拋出OutOfMemoryError異常。初始堆內(nèi)存大小為物理內(nèi)存的1/64。
如果應(yīng)用程序運(yùn)行在手機(jī)上或物理內(nèi)存小于192M時(shí),JVM默認(rèn)的初始堆內(nèi)存大小和最大堆內(nèi)存大小如下圖:

最大堆內(nèi)存為物理內(nèi)存的1/2,初始堆內(nèi)存大小為物理內(nèi)存的1/64,但當(dāng)初始堆內(nèi)存最小為8MB,則為8MB。
默認(rèn)空余堆內(nèi)存小于40%時(shí),JVM就會(huì)增大堆直到-Xmx的最大限制;空余堆內(nèi)存大于70%時(shí),JVM會(huì)減少堆直到 -Xms的最小限制。
因此,服務(wù)器一般設(shè)置-Xms、-Xmx相等以避免在每次GC后調(diào)整堆的大小。對(duì)象的堆內(nèi)存由稱為垃圾回收器的自動(dòng)內(nèi)存管理系統(tǒng)回收。
其中最大堆內(nèi)存是JVM使用內(nèi)存的上限,實(shí)際運(yùn)行過(guò)程中使用多少便是多少。默認(rèn),分配給年輕代的最大空間量是堆總大小的三分之一。
針對(duì)最開(kāi)始的問(wèn)題,如果每個(gè)程序都按照默認(rèn)配置啟動(dòng),一臺(tái)服務(wù)器上部署多個(gè)應(yīng)用時(shí),就會(huì)出現(xiàn)內(nèi)存吃緊的情況,造成一定的浪費(fèi)。最簡(jiǎn)單的操作就是在執(zhí)行java -jar啟動(dòng)時(shí)添加上對(duì)應(yīng)的jvm內(nèi)存設(shè)置參數(shù)。
java -Xms64m -Xmx128m -jar xxx.jar
項(xiàng)目使用的是Docker部署, 我們先來(lái)查看 原來(lái)的Dockerfile文件
確實(shí)沒(méi)有設(shè)置-Xms、-Xmx
#設(shè)置鏡像基礎(chǔ),jdk8 FROM java:8 #維護(hù)人員信息 MAINTAINER FLY #設(shè)置鏡像對(duì)外暴露端口 EXPOSE 8061 #將當(dāng)前 target 目錄下的 jar 放置在根目錄下,命名為 app.jar,推薦使用絕對(duì)路徑。 ADD target/certif-system-2.1.0.jar /certif-system-2.1.0.jar # 時(shí)區(qū)設(shè)置 RUN echo "Asia/shanghai" > /etc/timezone #執(zhí)行啟動(dòng)命令 ENTRYPOINT ["java", "-jar","/certif-system-2.1.0.jar"]
優(yōu)化
限制JVM內(nèi)存
#設(shè)置變量 JAVA_OPTS
ENV JAVA_OPTS=""#這樣寫會(huì)以shell方式執(zhí)行,會(huì)替換變量
ENTRYPOINT java ${JAVA_OPTS}-Djava.security.egd=file:/dev/./urandom -jar /app.jar
#下面這樣寫法不行,他只是拼接不會(huì)識(shí)別變量
#ENTRYPOINT ["java","${JAVA_OPTS}","-Djava.security.egd=file:/dev/./urandom","-jar","app.jar"]Spring Boot會(huì)將任何環(huán)境變量傳遞給應(yīng)用程序 - 但是我們的JAVA_OPTS并非是針對(duì)應(yīng)用程序的,而是針對(duì)Java runtime本身的。 所以我們需要使用$ JAVA_OPTS變量來(lái) exec java。
這需要對(duì)Dockerfile進(jìn)行一些小改動(dòng):
ENTRYPOINT exec java $JAVA_OPTS -jar app.jar
運(yùn)行docker run命令
意思是運(yùn)行時(shí)通過(guò)-e重置覆蓋環(huán)境變量中JAVA_OPTS參數(shù)信息。
docker run -e JAVA_OPTS='-Xmx1344M -Xms1344M -Xmn448M -XX:MaxMetaspaceSize=192M -XX:MetaspaceSize=192M'
參數(shù)解釋
JVM常見(jiàn)參數(shù)
- 可通過(guò)JAVA_OPTS設(shè)置:
參數(shù)說(shuō)明: -server:一定要作為第一個(gè)參數(shù),在多個(gè)CPU時(shí)性能佳 -Xms:初始Heap大小,使用的最小內(nèi)存,cpu性能高時(shí)此值應(yīng)設(shè)的大一些 -Xmx:java heap最大值,使用的最大內(nèi)存 -XX:PermSize:設(shè)定內(nèi)存的永久保存區(qū)域 -XX:MaxPermSize:設(shè)定最大內(nèi)存的永久保存區(qū)域 -XX:MaxNewSize: +XX:AggressiveHeap 會(huì)使得 Xms沒(méi)有意義。這個(gè)參數(shù)讓jvm忽略Xmx參數(shù),瘋狂地吃完一個(gè)G物理內(nèi)存,再吃盡一個(gè)G的swap。 -Xss:每個(gè)線程的Stack大小 -verbose:gc 現(xiàn)實(shí)垃圾收集信息 -Xloggc:gc.log 指定垃圾收集日志文件 -Xmn:young generation的heap大小,一般設(shè)置為Xmx的3、4分之一 -XX:+UseParNewGC :縮短minor收集的時(shí)間 -XX:+UseConcMarkSweepGC :縮短major收集的時(shí)間 提示:此選項(xiàng)在Heap Size 比較大而且Major收集時(shí)間較長(zhǎng)的情況下使用更合適。
java.security.egd 作用
SecureRandom在java各種組件中使用廣泛,可以可靠的產(chǎn)生隨機(jī)數(shù)。但在大量產(chǎn)生隨機(jī)數(shù)的場(chǎng)景下,性能會(huì)較低。這時(shí)可以使用"-Djava.security.egd=file:/dev/./urandom"加快隨機(jī)數(shù)產(chǎn)生過(guò)程。
建議在大量使用隨機(jī)數(shù)的時(shí)候,將隨機(jī)數(shù)發(fā)生器指定為/dev/./urandom。
bug產(chǎn)生的原因請(qǐng)注意下面第四行源碼,如果java.security.egd參數(shù)指定的是file:/dev/random或者file:/dev/urandom,則調(diào)用了無(wú)參的NativeSeedGenerator構(gòu)造函數(shù),而無(wú)參的構(gòu)造函數(shù)將默認(rèn)使用file:/dev/random 。openjdk的代碼和hotspot的代碼已經(jīng)不同,openjdk在后續(xù)產(chǎn)生隨機(jī)數(shù)的時(shí)候沒(méi)有使用這個(gè)變量。
abstract class SeedGenerator {
......
static {
String egdSource = SunEntries.getSeedSource();
if (egdSource.equals(URL_DEV_RANDOM) || egdSource.equals(URL_DEV_URANDOM)) {
try {
instance = new NativeSeedGenerator();
if (debug != null) {
debug.println("Using operating system seed generator");
}
} catch (IOException e) {
if (debug != null) {
debug.println("Failed to use operating system seed "
+ "generator: " + e.toString());
}
}
} else if (egdSource.length() != 0) {
try {
instance = new URLSeedGenerator(egdSource);
if (debug != null) {
debug.println("Using URL seed generator reading from "
+ egdSource);
}
} catch (IOException e) {
if (debug != null)
debug.println("Failed to create seed generator with "
+ egdSource + ": " + e.toString());
}
}
......
}
優(yōu)化后的Dockerfile文件
#設(shè)置基礎(chǔ)鏡像jdk8
FROM java:8
#維護(hù)人員信息
MAINTAINER FLY
#設(shè)置鏡像對(duì)外暴露端口
EXPOSE 8061
#將當(dāng)前 target 目錄下的 jar 放置在根目錄下,命名為 app.jar,推薦使用絕對(duì)路徑。
ADD target/certif-system-2.1.0.jar /certif-system-2.1.0.jar
# 設(shè)置環(huán)境變量
ENV JAVA_OPTS="-server -Xms512m -Xmx512m"
# 時(shí)區(qū)設(shè)置
RUN echo "Asia/shanghai" > /etc/timezone
#執(zhí)行啟動(dòng)命令
#ENTRYPOINT ["java", "-jar","/certif-system-2.1.0.jar"]
ENTRYPOINT exec java ${JAVA_OPTS} -Djava.security.egd=file:/dev/./urandom -jar /certif-system-2.1.0.jar優(yōu)化后的效果

JVM參數(shù)設(shè)置是否生效
通過(guò) docker exec -it 5a8ff3925974 ps -ef | grep java 查看
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS 5a8ff3925974 certif-system 0.74% 493.3MiB / 800MiB 61.66% 272kB / 304kB 7.54MB / 0B 97 [root@localhost certif]# docker exec -it 5a8ff3925974 ps -ef | grep java root 1 0 5 12:13 ? 00:01:02 java -server -Xms512m -Xmx51

基礎(chǔ)鏡像優(yōu)化
減少Spring Boot減少JVM占用的三種Dockerfile鏡像配置:
OpenJ9
OpenJ9:取代Hotspot的IBM Eclipse項(xiàng)目。它已經(jīng)被開(kāi)發(fā)很長(zhǎng)一段時(shí)間,看起來(lái)已經(jīng)足夠成熟,可以用于生產(chǎn)。
您可以立即輕松地獲益,替換一些基本鏡像和一些參數(shù)可能足以為您的應(yīng)用程序提供巨大的推動(dòng)力 - 我已經(jīng)通過(guò)更改 Dockerfile基本映像替換了一些應(yīng)用程序,節(jié)約了大約 1/3的內(nèi)存占用,增強(qiáng)了吞吐量。
FROM adoptopenjdk/openjdk8-openj9:alpine-slim COPY target/app.jar /my-app/app.jar ENTRYPOINT java $JAVA_OPTS -Xshareclasses -Xquickstart -jar /my-app/app.jar
GraalVM
GraalVM:圍繞這個(gè)由Oracle實(shí)驗(yàn)室開(kāi)發(fā)的有前途的虛擬機(jī)進(jìn)行了大量宣傳。它為您提供了將應(yīng)用程序編譯為本機(jī)鏡像的選項(xiàng),生成鏡像非常非常快且內(nèi)存消耗很少,吸引人眼球的另一個(gè)功能是能夠與多種語(yǔ)言(如Javascript,Ruby,Python和Java)進(jìn)行交互操作。
FROM oracle/graalvm-ce:1.0.0-rc15 COPY target/app.jar /my-app/app.jar ENTRYPOINT java $JAVA_OPTS -jar /my-app/app.jar
Fabric8
Fabric8 shell:一個(gè)bash腳本,可根據(jù)應(yīng)用程序當(dāng)前運(yùn)行環(huán)境自動(dòng)為您配置JVM參數(shù)。它可以在這里下載,是這個(gè)研究項(xiàng)目的產(chǎn)物。它降低了不少內(nèi)存:
FROM java:openjdk-8-alpine
COPY target/app.jar /my-app/app.jar
COPY run-java.sh /my-app/run-java.sh
ENTRYPOINT JAVA_OPTIONS=${JAVA_OPTS} JAVA_APP_JAR=/my-app/app.jar /my-app/run-java.sh雖然我們?cè)趹?yīng)用解決方案時(shí)總是需要考慮上下文,但對(duì)我來(lái)說(shuō),獲勝者是OpenJ9,從而以最少的配置實(shí)現(xiàn)了生產(chǎn)就緒的性能和內(nèi)存占用。
雖然仍然沒(méi)有找到使用不合適的情況,但這并不意味著它將成為一個(gè)銀彈解決方案,請(qǐng)記住,最好是測(cè)試替代品,看看哪種更適合您的需求。
優(yōu)化后的Dockerfile文件
#設(shè)置鏡像基礎(chǔ),jdk8
FROM adoptopenjdk/openjdk8-openj9:alpine-slim
#維護(hù)人員信息
MAINTAINER FLY
#設(shè)置鏡像對(duì)外暴露端口
EXPOSE 8061
#將當(dāng)前 target 目錄下的 jar 放置在根目錄下,命名為 app.jar,推薦使用絕對(duì)路徑。
ADD target/certif-system-2.1.0.jar /certif-system-2.1.0.jar
# 設(shè)置環(huán)境變量
ENV JAVA_OPTS="-server -Xms512m -Xmx512m"
# 時(shí)區(qū)設(shè)置
RUN echo "Asia/shanghai" > /etc/timezone
#執(zhí)行啟動(dòng)命令
#ENTRYPOINT ["java", "-jar","/certif-system-2.1.0.jar"]
#ENTRYPOINT exec java ${JAVA_OPTS} -Djava.security.egd=file:/dev/./urandom -jar /certif-system-2.1.0.jar
#ENTRYPOINT java $JAVA_OPTS -Xshareclasses -Xquickstart -jar /certif-system-2.1.0.jar
ENTRYPOINT java $JAVA_OPTS -Xshareclasses -Xquickstart -jar /certif-system-2.1.0.jar優(yōu)化后的效果

備注
Xmx < limit
docker鏡像的內(nèi)存上限,不能全部給“-Xmx”。因?yàn)镴VM消耗的內(nèi)存不僅僅是Heap,如下圖:
JVM基礎(chǔ)結(jié)構(gòu)如下:棧、堆。

棧:
- JVM中的棧主要是指線程里面的棧,里面有方法棧、native方法棧、PC寄存器等等;每個(gè)方法棧是由棧幀組成的;每個(gè)棧幀是由局部變量表、操作數(shù)棧等組成。
- 每個(gè)棧幀其實(shí)就代表一個(gè)方法
堆:
- java中所有對(duì)象都在堆中分配;堆中對(duì)象又分為年輕代、老年代等等,不同代的對(duì)象使用不同垃圾回收算法。
- -XMs:?jiǎn)?dòng)虛擬機(jī)預(yù)留的內(nèi)存 -Xmx:最大的堆內(nèi)存
因此
JVM = Heap + Method Area + Constant Pool + Thread Stack * num of thread
所以Xmx的值要小于鏡像上限內(nèi)存。
支持springboot多環(huán)境和jvm動(dòng)態(tài)配置的Dockerfile
假設(shè)springboot項(xiàng)目 myboot-api , 在其根目錄下創(chuàng)建文件Dockerfile
內(nèi)容如下:
FROM java:8
MAINTAINER xxx
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo 'Asia/Shanghai' >/etc/timezone
ENV LANG=zh_CN.UTF-8 \
JAVA_OPTS="-server -Xms512m -Xmx512m" \
SPRING_PROFILES_ACTIVE="dev"
#ARG JAR_FILE
#ADD ${JAR_FILE} app.jar
ADD target/myboot-api.jar app.jar
ENTRYPOINT exec java ${JAVA_OPTS} -Djava.security.egd=file:/dev/./urandom -jar -Dspring.profiles.active=${SPRING_PROFILES_ACTIVE} /app.jar
其中ENV 環(huán)境變量
- JAVA_OPTS JVM堆內(nèi)存起始最大值配置
- SPRING_PROFILES_ACTIVE application.yml環(huán)境
Linux 命令行創(chuàng)建鏡像 啟動(dòng)容器
echo "===============動(dòng)態(tài)參數(shù)配置 begin===============>" APPLICATION_NAME=xxx-srm-api echo "image and container name is $APPLICATION_NAME" # springboot啟動(dòng)的端口號(hào) BootPort=8082 echo "the spring boot ($APPLICATION_NAME) port is $BootPort" # docker中的springboot啟動(dòng)的端口號(hào) DockerBootPort=8082 echo "===============動(dòng)態(tài)參數(shù)配置 end===============>" echo "build docker image" # mvn dockerfile:build docker build -f Dockerfile -t $APPLICATION_NAME:latest . echo "current docker images:" docker images | grep $APPLICATION_NAME echo "start container ===============> " docker run -d -p $BootPort:$DockerBootPort -e JAVA_OPTS="-server -Xms512m -Xmx512m" -e SPRING_PROFILES_ACTIVE="test" --name $APPLICATION_NAME $APPLICATION_NAME:latest
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
- Java?SpringBoot內(nèi)存泄漏問(wèn)題與解決過(guò)程
- SpringBoot項(xiàng)目啟動(dòng)內(nèi)存占用過(guò)高問(wèn)題及解決
- SpringBoot中排查內(nèi)存泄漏的方法小結(jié)
- SpringBoot+Spring Security基于內(nèi)存用戶認(rèn)證的實(shí)現(xiàn)
- SpringBoot項(xiàng)目啟動(dòng)數(shù)據(jù)加載內(nèi)存的三種方法
- SpringBoot使用Caffeine實(shí)現(xiàn)內(nèi)存緩存示例詳解
- SpringBoot?SpringSecurity?詳細(xì)介紹(基于內(nèi)存的驗(yàn)證)
相關(guān)文章
多個(gè)docker compose啟動(dòng)的容器之間通信實(shí)現(xiàn)過(guò)程
文章討論了Docker Compose中多組容器編排的問(wèn)題,指出默認(rèn)情況下各組容器網(wǎng)絡(luò)會(huì)隔離,為了解決跨組訪問(wèn)的問(wèn)題,文章建議創(chuàng)建一個(gè)公共網(wǎng)絡(luò),并將各組容器組的默認(rèn)網(wǎng)絡(luò)配置改為該公共網(wǎng)絡(luò),同時(shí)啟用外部連接,這樣可以實(shí)現(xiàn)跨組訪問(wèn),解決網(wǎng)絡(luò)隔離的限制2025-09-09
使用Docker部署MySQL數(shù)據(jù)庫(kù)的兩種方法
在現(xiàn)代軟件開(kāi)發(fā)中,MySQL 是一種流行的關(guān)系數(shù)據(jù)庫(kù)管理系統(tǒng),因其可靠性和易用性受到廣泛歡迎,通過(guò) Docker,可以快速、便捷地部署和管理 MySQL 數(shù)據(jù)庫(kù)實(shí)例,本文將介紹兩種通過(guò) Docker 部署 MySQL 的方法,需要的朋友可以參考下2024-10-10
docker配置阿里云鏡像倉(cāng)庫(kù)的實(shí)現(xiàn)
本文主要介紹了docker配置阿里云鏡像倉(cāng)庫(kù)的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08
docker 編輯Dockerfile 添加php7.2 acpu的問(wèn)題
這篇文章主要介紹了docker 編輯Dockerfile 添加php7.2 acpu問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07
Docker Gitlab+Jenkins+Harbor構(gòu)建持久化平臺(tái)操作
這篇文章主要介紹了Docker Gitlab+Jenkins+Harbor構(gòu)建持久化平臺(tái)操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-11-11
Docker容器實(shí)現(xiàn)MySQL多源復(fù)制場(chǎng)景分析
這篇文章主要介紹了Docker容器實(shí)現(xiàn)MySQL多源復(fù)制,通過(guò)本文學(xué)習(xí)可以掌握多源復(fù)制的好處,通過(guò)使用場(chǎng)景分析給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-06-06
一文教會(huì)你在Docker容器中實(shí)現(xiàn)Mysql主從復(fù)制
MySQL的主從復(fù)制之前也沒(méi)做過(guò),剛百度了下發(fā)現(xiàn)并不算難,所以下面這篇文章主要給大家介紹了關(guān)于在Docker容器中實(shí)現(xiàn)Mysql主從復(fù)制的相關(guān)資料,文中通過(guò)圖文介紹的非常詳細(xì),需要的朋友可以參考下2022-11-11

