Docker环境下Spring Boot应用内存飙升分析与解决场景分析

目录
  • Spring Boot应用内存飙升
    • 服务现状
    • JVM默认内存设置
  • 优化
    • 限制JVM内存
    • 参数解释
      • JVM常见参数
      • java.security.egd 作用
    • 优化后的Dockerfile文件
    • 优化后的效果
      • JVM参数设置是否生效
    • 基础镜像优化
    • 优化后的Dockerfile文件
    • 优化后的效果
  • 备注
    • Xmx < limit
    • 支持springboot多环境和jvm动态配置的Dockerfile
  • 参考

Spring Boot应用内存飙升

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

服务现状

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

JVM默认内存设置

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

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

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

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

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

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

最大堆内存为物理内存的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

优化后的效果

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

基础镜像优化

减少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

优化后的效果

备注

Xmx < limit

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

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


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

参考

https://medium.com/@cl4r1ty/docker-spring-boot-and-java-opts-ba381c818fa2

到此这篇关于Docker环境下Spring Boot应用内存飙升分析与解决的文章就介绍到这了,更多相关Docker Spring Boot内存内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Spring Boot 2.4新特性减少95%内存占用问题

    节省 95%的内存占用,减少 80%的启动耗时. GraalVM 是一种高性能的虚拟机,它可以显著的提高程序的性能和运行效率,非常适合微服务.最近比较火的 Java 框架 Quarkus 默认支持 GraalVM 下图为 Quarkus 和传统框架(SpringBoot) 等对比图,更快的启动速度.更小的内存消耗.更短的服务响应. Spring Boot 2.4 开始逐步提供对 GraalVM 的支持,旨在提升上文所述的 启动.内存.响应的使用体验. 安装 GraalVM 目前官方社区版本最新为

  • springboot配置内存数据库H2教程详解

    业务背景:因soa系统要供外网访问,处于安全考虑用springboot做了个前置模块,用来转发外网调用的请求和soa返回的应答.其中外网的请求接口地址在DB2数据库中对应专门的一张表来维护,要是springboot直接访问数据库,还要专门申请权限等,比较麻烦,而一张表用内置的H2数据库维护也比较简单,就可以作为替代的办法. 环境:springboot+maven3.3+jdk1.7 1.springboot的Maven工程结构 说明一下,resource下的templates文件夹没啥用.我忘记

  • springboot整合H2内存数据库实现单元测试与数据库无关性

    一.新建spring boot工程 新建工程的时候,需要加入JPA,H2依赖 二.工程结构 pom文件依赖如下: <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:s

  • Docker环境下Spring Boot应用内存飙升分析与解决场景分析

    目录 Spring Boot应用内存飙升 服务现状 JVM默认内存设置 优化 限制JVM内存 参数解释 JVM常见参数 java.security.egd 作用 优化后的Dockerfile文件 优化后的效果 JVM参数设置是否生效 基础镜像优化 OpenJ9 GraalVM Fabric8 优化后的Dockerfile文件 优化后的效果 备注 Xmx < limit 支持springboot多环境和jvm动态配置的Dockerfile 参考 Spring Boot应用内存飙升 一个简单的Spr

  • SpringBoot内存数据导出成Excel的实现方法

    前言 这是本人写的一个SpringBoot对Excel写入的方法,实测能用,待提升的地方有很多,有不足之处请多多指点. Excel2003版(后缀为.xls)最大行数是65536行,最大列数是256列. Excel2007以上的版本(后缀为.xlsx)最大行数是1048576行,最大列数是16384列. 若数据量超出行数,需要进行脚页的控制,这一点没做,因为一般100W行已够用. 提供3种方法写入: 1.根据给定的实体类列List和列名数组arr[]进行Excel写入 2.根据给定的List和k

  • Spring Boot整合流控组件Sentinel的场景分析

    目录 一.百度百科 1.Sentinel 特性 2.Sentinel 的开源生态 二.Sentinel 的历史 三.Sentinel 基本概念 1.资源 2.规则 四.sentinel的优势 五.Sentinel 功能和设计理念 1.什么是流量控制 2.流量控制设计理念 3.什么是熔断降级 4.熔断降级设计理念 5.系统自适应保护 6.Sentinel 是如何工作的 7.竞品对比 六.SpringBoot整合sentinel 1.加入pom 2.编写sentinel规则 3.测试 七.sprin

  • Spring Boot配置线程池拒绝策略的场景分析(妥善处理好溢出的任务)

    目录 场景重现 配置拒绝策略 代码示例 通过之前三篇关于Spring Boot异步任务实现的博文,我们分别学会了用@Async创建异步任务.为异步任务配置线程池.使用多个线程池隔离不同的异步任务.今天这篇,我们继续对上面的知识进行完善和优化! 如果你已经看过上面几篇内容并已经掌握之后,一起来思考下面这个问题: 假设,线程池配置为核心线程数2.最大线程数2.缓冲队列长度2.此时,有5个异步任务同时开始,会发生什么? 场景重现 我们先来把上面的假设用代码实现一下: 第一步:创建Spring Boot

  • spring boot 动态生成接口实现类的场景分析

    目录 一: 定义注解 二: 建立动态代理类 三: 注入spring容器 四: 编写拦截器 五: 新建测试类 在某些业务场景中,我们只需要业务代码中定义相应的接口或者相应的注解,并不需要实现对应的逻辑. 比如 mybatis和feign: 在 mybatis 中,我们只需要定义对应的mapper接口:在 feign 中,我们只需要定义对应业务系统中的接口即可. 那么在这种场景下,具体的业务逻辑时怎么执行的呢,其实原理都是动态代理. 我们这里不具体介绍动态代理,主要看一下它在springboot项目

  • Spring Boot整合Zookeeper实现分布式锁的场景分析

    目录 一.Java当中关于锁的概念 1.1.什么是锁 1.2.锁的使用场景 1.3.什么是分布式锁 1.4.分布式锁的使用场景 二.zk实现分布式锁 2.1.zk中锁的种类: 2.2.zk如何上读锁 2.3.zk如何上写锁 2.4.⽺群效应 三.springboot整合分布式锁 温馨提示:本篇文章要求掌握zk的数据结构,以及临时序号节点! zk实现分布式锁完全是依靠zk节点类型当中的临时序号节点来实现的 一.Java当中关于锁的概念 1.1.什么是锁 锁是用来控制多个线程访问共享资源的方式,一般

  • macOS下Spring Boot开发环境搭建教程

    macOS搭建Spring Boot开发环境,具体内容如下 软硬件环境 macOS Sierra java 1.8.0_65 maven 3.5.0 idea 2017.1.5 前言 最近接触了一点java web相关的知识,了解一下最近比较火的开发框架Spring Boot,站在一个从未涉足过java web和spring的开发者角度来讲,spring boot确实是一个非常不错的框架,配置简单,容易入门,对于想入行java web的童鞋,是一个很好的切入点. maven安装 这里选择mave

  • Docker容器化spring boot应用详解

    前置条件 容器化spring boot应用所需环境: jdk 1.8 + maven 3.0 + 我们的需求是:使用maven打包,将spring boot应用制作成docker镜像并上传到docker hub.在其他机器上,可以直接docker pull并运行容器. 创建spring boot应用 spring boot 包结构为: └── src └── main └── java └── me └── ithakar 创建spring boot Application主类,src/main

  • docker环境下数据库的备份(postgresql, mysql) 实例代码

    posgresql 备份/恢复 1.备份 DATE=`date +%Y%m%d-%H%M` BACK_DATA=xxapp-data-${DATE}.out # 这里设置备份文件的名字, 加入日期是为了防止重复 docker exec pg-db pg_dumpall -U postgres > ${BACK_DATA} # pg-db 是数据库的 docker 名称 2.恢复 docker cp ${BACK_DATA} pg-db:/tmp docker exec pg-db psql -U

  • docker环境下安装jenkins容器的详细教程

    推荐docker学习资料:https://www.runoob.com/docker/docker-tutorial.html 一.Centos7环境 docker安装 先到官网下载镜像,docker镜像官方:https://hub.docker.com/ 1.最新版安装 yum install -y yum-utils device-mapper-persistent-data lvm2 2.加入docker源 yum-config-manager --add-repo https://mir

  • docker环境下分布式运行jmeter的教程详解

    1.构建jmeter的基础镜像 dockerfile文件如下: # Use Java 8 slim JRE FROM openjdk:8-jre-slim MAINTAINER QJP # JMeter version ARG JMETER_VERSION=5.1.1 # Install few utilities RUN apt-get clean && \ apt-get update && \ apt-get -qy install \ wget \ telnet \

随机推荐