温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

Docker环境下Spring Boot应用内存飙升的原因

发布时间:2021-08-24 15:07:05 来源:亿速云 阅读:330 作者:chen 栏目:开发技术

本篇内容介绍了“Docker环境下Spring Boot应用内存飙升的原因”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

目录
  • Spring Boot应用内存飙升

    • 服务现状

    • JVM默认内存设置

  • 优化

    • 限制JVM内存

    • 参数解释

      • JVM常见参数

      • java.security.egd 作用

    • 优化后的Dockerfile文件

      • 优化后的效果

        • JVM参数设置是否生效

      • 基础镜像优化

        • OpenJ9

        • GraalVM

        • Fabric8

      • 优化后的Dockerfile文件

        • 优化后的效果

        • 备注

          • Xmx < limit

            • 支持springboot多环境和jvm动态配置的Dockerfile


          Docker环境下Spring Boot应用内存飙升的原因

          Spring Boot应用内存飙升

          一个简单的Spring Boot应用, 几乎只有一个用户在用,内存竟然达到1.2G, 可怕

          Docker环境下Spring Boot应用内存飙升的原因

          服务现状

          由于之前服务比较少,服务器资源充足,许多服务启动时都未添加JVM参数(遗留问题)。结果就是每个服务启动都占用了1.2G-2G的内存,有些服务的体量根本用不了这么多。那么,在Spring Boot中如果未设置JVM内存参数时,JVM内存是如何配置的呢?

          JVM默认内存设置

          当运行一个Spring Boot项目时,如果未设置JVM内存参数,Spring Boot默认会采用JVM自身默认的配置策略。在资源比较充足的情况下,开发者倒是不太用关心内存的设置。但一旦涉及到资源不足,JVM优化,那么就需要了解默认的JVM内存配置策略。

          关于JVM内存最常见的设置为初始堆大小(-Xms)和最大堆内存(-Xmx)。很多人懒得去设置,而是采用JVM的默认值。特别是在开发环境下,如果启动的微服务比较多,内存会被撑爆。

          而JVM默认内存配置策略分两种场景,大内存空间场景和小内存空间场景(小于192M)。

          以4GB内存为例,初始堆内存大小和最大堆内存大小如下图:

          Docker环境下Spring Boot应用内存飙升的原因

          默认情况下,最大堆内存占用物理内存的1/4,如果应用程序超过该上限,则会抛出OutOfMemoryError异常。初始堆内存大小为物理内存的1/64

          如果应用程序运行在手机上或物理内存小于192M时,JVM默认的初始堆内存大小和最大堆内存大小如下图:

          Docker环境下Spring Boot应用内存飙升的原因

          最大堆内存为物理内存的1/2,初始堆内存大小为物理内存的1/64,但当初始堆内存最小为8MB,则为8MB。

          默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。

          因此,服务器一般设置-Xms、-Xmx相等以避免在每次GC后调整堆的大小。对象的堆内存由称为垃圾回收器的自动内存管理系统回收。

          其中最大堆内存是JVM使用内存的上限,实际运行过程中使用多少便是多少。默认,分配给年轻代的最大空间量是堆总大小的三分之一。

          针对最开始的问题,如果每个程序都按照默认配置启动,一台服务器上部署多个应用时,就会出现内存吃紧的情况,造成一定的浪费。最简单的操作就是在执行java -jar启动时添加上对应的jvm内存设置参数。

          java -Xms64m -Xmx128m -jar xxx.jar

          项目使用的是Docker部署, 我们先来查看 原来的Dockerfile文件

          确实没有设置-Xms、-Xmx

          #设置镜像基础,jdk8
          FROM java:8
          #维护人员信息
          MAINTAINER FLY
          #设置镜像对外暴露端口
          EXPOSE 8061
          #将当前 target 目录下的 jar 放置在根目录下,命名为 app.jar,推荐使用绝对路径。
          ADD target/certif-system-2.1.0.jar /certif-system-2.1.0.jar
          # 时区设置
          RUN echo "Asia/shanghai" > /etc/timezone
          #执行启动命令
          ENTRYPOINT ["java", "-jar","/certif-system-2.1.0.jar"]

          优化

          限制JVM内存

          #设置变量 JAVA_OPTS
           
          ENV JAVA_OPTS=""#这样写会以shell方式执行,会替换变量
           
          ENTRYPOINT java ${JAVA_OPTS}-Djava.security.egd=file:/dev/./urandom -jar /app.jar
           
          #下面这样写法不行,他只是拼接不会识别变量
           
          #ENTRYPOINT ["java","${JAVA_OPTS}","-Djava.security.egd=file:/dev/./urandom","-jar","app.jar"]

          Spring Boot会将任何环境变量传递给应用程序 - 但是我们的JAVA_OPTS并非是针对应用程序的,而是针对Java runtime本身的。 所以我们需要使用$ JAVA_OPTS变量来 exec java。 这需要对Dockerfile进行一些小改动:
          ENTRYPOINT exec java $JAVA_OPTS -jar app.jar

          运行docker run命令

          意思是运行时通过-e重置覆盖环境变量中JAVA_OPTS参数信息。

          docker run  -e  JAVA_OPTS='-Xmx1344M -Xms1344M -Xmn448M -XX:MaxMetaspaceSize=192M -XX:MetaspaceSize=192M'

          参数解释

          JVM常见参数

          可通过JAVA_OPTS设置

          参数说明:
          -server:一定要作为第一个参数,在多个CPU时性能佳
          -Xms:初始Heap大小,使用的最小内存,cpu性能高时此值应设的大一些
          -Xmx:java heap最大值,使用的最大内存
          -XX:PermSize:设定内存的永久保存区域
          -XX:MaxPermSize:设定最大内存的永久保存区域
          -XX:MaxNewSize:
          +XX:AggressiveHeap 会使得 Xms没有意义。这个参数让jvm忽略Xmx参数,疯狂地吃完一个G物理内存,再吃尽一个G的swap。
          -Xss:每个线程的Stack大小
          -verbose:gc 现实垃圾收集信息
          -Xloggc:gc.log 指定垃圾收集日志文件
          -Xmn:young generation的heap大小,一般设置为Xmx的3、4分之一
          -XX:+UseParNewGC :缩短minor收集的时间
          -XX:+UseConcMarkSweepGC :缩短major收集的时间
          提示:此选项在Heap Size 比较大而且Major收集时间较长的情况下使用更合适。

          java.security.egd 作用

          SecureRandom在java各种组件中使用广泛,可以可靠的产生随机数。但在大量产生随机数的场景下,性能会较低。这时可以使用"-Djava.security.egd=file:/dev/./urandom"加快随机数产生过程。

          建议在大量使用随机数的时候,将随机数发生器指定为/dev/./urandom

          bug产生的原因请注意下面第四行源码,如果java.security.egd参数指定的是file:/dev/random或者file:/dev/urandom,则调用了无参的NativeSeedGenerator构造函数,而无参的构造函数将默认使用file:/dev/random 。openjdk的代码和hotspot的代码已经不同,openjdk在后续产生随机数的时候没有使用这个变量。

          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());
                      }
                  }
          ......
              }

          优化后的Dockerfile文件

          #设置基础镜像jdk8
          FROM java:8
          #维护人员信息
          MAINTAINER FLY
          #设置镜像对外暴露端口
          EXPOSE 8061
          #将当前 target 目录下的 jar 放置在根目录下,命名为 app.jar,推荐使用绝对路径。
          ADD target/certif-system-2.1.0.jar /certif-system-2.1.0.jar
          # 设置环境变量
          ENV JAVA_OPTS="-server -Xms512m -Xmx512m"
          # 时区设置
          RUN echo "Asia/shanghai" > /etc/timezone
          #执行启动命令
          #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

          优化后的效果

          Docker环境下Spring Boot应用内存飙升的原因

          JVM参数设置是否生效

          通过 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

          Docker环境下Spring Boot应用内存飙升的原因

          基础镜像优化

          减少Spring Boot减少JVM占用的三种Dockerfile镜像配置:

          OpenJ9

          OpenJ9:取代Hotspot的IBM Eclipse项目。它已经被开发很长一段时间,看起来已经足够成熟,可以用于生产。您可以立即轻松地获益,替换一些基本镜像和一些参数可能足以为您的应用程序提供巨大的推动力 - 我已经通过更改 Dockerfile基本映像替换了一些应用程序,节约了大约 1/3的内存占用,增强了吞吐量。

          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:围绕这个由Oracle实验室开发的有前途的虚拟机进行了大量宣传。它为您提供了将应用程序编译为本机镜像的选项,生成镜像非常非常快且内存消耗很少,吸引人眼球的另一个功能是能够与多种语言(如Javascript,Ruby,Python和Java)进行交互操作。

          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:一个bash脚本,可根据应用程序当前运行环境自动为您配置JVM参数。它可以在这里下载,是这个研究项目的产物。它降低了不少内存:

          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

          虽然我们在应用解决方案时总是需要考虑上下文,但对我来说,获胜者是OpenJ9,从而以最少的配置实现了生产就绪的性能和内存占用。

          虽然仍然没有找到使用不合适的情况,但这并不意味着它将成为一个银弹解决方案,请记住,最好是测试替代品,看看哪种更适合您的需求。

          优化后的Dockerfile文件

          #设置镜像基础,jdk8
          FROM adoptopenjdk/openjdk8-openj9:alpine-slim
          #维护人员信息
          MAINTAINER FLY
          #设置镜像对外暴露端口
          EXPOSE 8061
          #将当前 target 目录下的 jar 放置在根目录下,命名为 app.jar,推荐使用绝对路径。
          ADD target/certif-system-2.1.0.jar /certif-system-2.1.0.jar
          # 设置环境变量
          ENV JAVA_OPTS="-server -Xms512m -Xmx512m"
          # 时区设置
          RUN echo "Asia/shanghai" > /etc/timezone
          #执行启动命令
          #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

          优化后的效果

          Docker环境下Spring Boot应用内存飙升的原因

          备注

          Xmx < limit

          docker镜像的内存上限,不能全部给“-Xmx”。因为JVM消耗的内存不仅仅是Heap,如下图:

          JVM基础结构如下:栈、堆。

          Docker环境下Spring Boot应用内存飙升的原因


          JVM中的栈主要是指线程里面的栈,里面有方法栈、native方法栈、PC寄存器等等;每个方法栈是由栈帧组成的;每个栈帧是由局部变量表、操作数栈等组成。

          每个栈帧其实就代表一个方法


          java中所有对象都在堆中分配;堆中对象又分为年轻代、老年代等等,不同代的对象使用不同垃圾回收算法。

          -XMs:启动虚拟机预留的内存 -Xmx:最大的堆内存

          因此

          JVM = Heap + Method Area + Constant Pool + Thread Stack * num of thread
          所以Xmx的值要小于镜像上限内存。

          支持springboot多环境和jvm动态配置的Dockerfile

          假设springboot项目 myboot-api , 在其根目录下创建文件Dockerfile
          内容如下:

          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 环境变量

          JAVA_OPTS JVM堆内存起始最大值配置
          SPRING_PROFILES_ACTIVE application.yml环境

          Linux 命令行创建镜像 启动容器

          echo "===============动态参数配置 begin===============>"
          APPLICATION_NAME=xxx-srm-api
          echo "image and container name is $APPLICATION_NAME"
           
          # springboot启动的端口号
          BootPort=8082
          echo "the spring boot ($APPLICATION_NAME) port is $BootPort"
           
          # docker中的springboot启动的端口号
          DockerBootPort=8082
           
          echo "===============动态参数配置 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

          “Docker环境下Spring Boot应用内存飙升的原因”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注亿速云网站,小编将为大家输出更多高质量的实用文章!

          向AI问一下细节

          免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

          AI