早期时候,容器内运行Java应用程序时,Jvm无法感知容器环境存在,所以对容器资源的限制比如内存或者cpu等都无法生效。原因是容器的资源管理使用了操作系统cgroup机制,但是Jvm无法感知cgroup。所以可能需要在jvm以及docker中指定两次内存限制。后来,在Jvm9及以后,Jvm开始了对容器资源限制的支持。在Jvm11中,可以使用-XX:+UseContainerSupport参数来制定Jvm使用容器内存。另外还有两个参数-XX:InitialRAMPercentage -XX:MaxRAMPercentage来制定Jvm使用容器内存的百分比。这里就以这三个参数为例,写一个简单的demo。

Java应用使用spring-boot:

@RequestMapping(value = "/hello")
public String hello() {
    List<byte[]> list = new ArrayList<>();
    for (int i = 0; i < 50; i++) {
        list.add(new byte[1024 * 1024 * 200]);
        System.out.println("use " + 200 * (i + 1) + " M memory");
    }
    return "hello";
}

这里只给出http接口的简单定义,循环分配200M的内存。

DockerFile:

FROM centos:latest

ADD target/test-maven-api.jar /app.jar
ADD ./jdk-11.0.1 /var/local/jdk11
ENV JAVA_HOME /var/local/jdk11/bin
ENV PATH=$PATH:$JAVA_HOME
EXPOSE 8080
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:InitialRAMPercentage=50 -XX:MaxRAMPercentage=80"
ENTRYPOINT java ${JAVA_OPTS} -jar /app.jar

因为对docker不太熟,简单写了一个DockerFile。使用centos作为基础镜像,然后将本地的jdk11导入到容器中。本来想直接基于jdk的基础镜像来做,但是仅有jdk9基础镜像,不知道是不是因为openjdk的原因。其次就是Java环境变量的设置以及定义了Java启动命令。这里定义了该Java应用的堆内存只能使用容器内50%-80%的内存。使用如下命令创建镜像并运行容器,限制了容器内存为1G:

docker build --rm -f "Dockerfile" -t java-springboot:latest .
docker run -m 1G -d -p 8080:8080 java-springboot

使用docker ps确认容器启动成功:

然后访问浏览器地址,并使用如下命令查看程序日志:

docker logs -f -t --tail 500 dd272

从这里可以看到,在申请800M内存时Jvm抛出OutOfMemory异常,这与预期相符,因为容器最大内存限制为1G,Jvm堆内存最大使用80%,也就是800M,所以申请800M内存失败了(Jvm本身及程序其他部分也会消耗一定的堆内存)。说明-XX:+UseContainerSupport参数确实生效了。

下面再看看不使用-XX:+UseContainerSupport的情况。我们仅需要替换之前DockerFile里的JAVA_OPTS参数即可:

ENV JAVA_OPTS="-Xms4g -Xmx4g"

看下日志:

这里申请的内存量超过了容器1G内存的限制,说明不使用-XX:+UseContainerSupport参数时Jvm无法感知容器内存限制的存在。并且由于超过容器内存限制,Jvm进程被容器kill了。至于为什么是1.6G时被kill了,原因还没有深究,应该与容器内存分配策略有关。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐