Docker 和 K8S 的兴起, 很多服务已经运行在容器环境, 对于 java 程序, JVM 设置是一个重要的环节. 这里总结下我们项目里的最佳实践.
Java Heap 基础知识
默认情况下, jvm 自动分配的 heap 大小取决于机器配置, 比如我们到一台 64G 内存服务器:
- java -XX:+PrintFlagsFinal -version | grep -Ei "maxheapsize|maxram"
- uintx DefaultMaxRAMFraction = 4 {product}
- uintx MaxHeapSize := 16875782144 {product}
- uint64_t MaxRAM = 137438953472 {pd product}
- uintx MaxRAMFraction = 4 {product}
- double MaxRAMPercentage = 25.000000 {product}
- java version "1.8.0_192"
- Java(TM) SE Runtime Environment (build 1.8.0_192-b12)
- Java HotSpot(TM) 64-Bit Server VM (build 25.192-b12, mixed mode)
可以看到, JVM 分配的最大 MaxHeapSize 为 16G, 计算公式如下:
MaxHeapSize = MaxRAM * 1 / MaxRAMFraction
MaxRAMFraction 默认是 4, 意味着, 每个 jvm 最多使用 25% 的机器内存.
但是需要注意的是, JVM 实际使用的内存会比 heap 内存大:
JVM 内存 = heap 内存 + 线程 stack 内存 (XSS) * 线程数 + 启动开销(constant overhead)
默认的 XSS 通常在 256KB 到 1MB, 也就是说每个线程会分配最少 256K 额外的内存, constant overhead 是 JVM 分配的其他内存.
我们可以通过 - Xmx 指定最大堆大小.
- java -XX:+PrintFlagsFinal -Xmx1g -version | grep -Ei "maxheapsize|maxram"
- uintx DefaultMaxRAMFraction = 4 {product}
- uintx MaxHeapSize := 1073741824 {product}
- uint64_t MaxRAM = 137438953472 {pd product}
- uintx MaxRAMFraction = 4 {product}
- double MaxRAMPercentage = 25.000000 {product}
- java version "1.8.0_192"
- Java(TM) SE Runtime Environment (build 1.8.0_192-b12)
- Java HotSpot(TM) 64-Bit Server VM (build 25.192-b12, mixed mode)
此外, 还可以使用 XX:MaxRAM 来指定.
java -XX:+PrintFlagsFinal -XX:MaxRAM=1g -version | grep -Ei
但是指定 - Xmx 或者 MaxRAM 需要了解机器的内存, 更好的方式是设置 MaxRAMFraction, 以下是不同的 Fraction 对应的可用内存比例:
- +----------------+-------------------+
- | MaxRAMFraction | % of RAM for heap |
- |----------------+-------------------|
- | 1 | 100% |
- | 2 | 50% |
- | 3 | 33% |
- | 4 | 25% |
- +----------------+-------------------+
容器环境的 Java Heap
容器环境, 由于 java 获取不到容器的内存限制, 只能获取到服务器的配置:
- $ docker run --rm alpine free -m
- total used free shared buffers cached
- Mem: 1998 1565 432 0 8 1244
- $ docker run --rm -m 256m alpine free -m
- total used free shared buffers cached
- Mem: 1998 1552 445 1 8 1244
这样容易引起不必要问题, 例如限制容器使用 100M 内存, 但是 jvm 根据服务器配置来分配初始化内存, 导致 java 进程超过容器限制被 kill 掉. 为了解决这个问题, 可以设置 - Xmx 或者 MaxRAM 来解决, 但就想第一部分描述的一样, 这样太不优雅了!
为了解决这个问题, Java 10 引入了 +UseContainerSupport(默认情况下启用), 通过这个特性, 可以使得 JVM 在容器环境分配合理的堆内存. 并且, 在 JDK8U191 版本之后, 这个功能引入到了 JDK 8, 而 JDK 8 是广为使用的 JDK 版本.
UseContainerSupport
-XX:+UseContainerSupport 允许 JVM 从主机读取 cgroup 限制, 例如可用的 CPU 和 RAM, 并进行相应的配置. 这样当容器超过内存限制时, 会抛出 OOM 异常, 而不是杀死容器.
该特性在 Java 8u191 +,10 及更高版本上可用.
注意, 在 191 版本后,-XX:{Min|Max}RAMFraction 被弃用, 引入了 - XX:MaxRAMPercentage, 其值介于 0.0 到 100.0 之间, 默认值为 25.0.
最佳实践
拉取最新的 openjdk:8-jre-alpine 作为底包, 截止这篇博客, 最新的版本是 212,>191
- docker run -it --rm openjdk:8-jre-alpine java -version
- openjdk version "1.8.0_212"
- OpenJDK Runtime Environment (IcedTea 3.12.0) (Alpine 8.212.04-r0)
- OpenJDK 64-Bit Server VM (build 25.212-b04, mixed mode)
我们构建一个基础镜像, dockerfile 如下:
- FROM openjdk:8-jre-alpine
- MAINTAINER jadepeng
- RUN echo "http://mirrors.aliyun.com/alpine/v3.6/main"> /etc/apk/repositories \
- && echo "http://mirrors.aliyun.com/alpine/v3.6/community">> /etc/apk/repositories \
- && apk update upgrade \
- && apk add --no-cache procps unzip curl bash tzdata \
- && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
- && echo "Asia/Shanghai"> /etc/timezone
- RUN apk add --update ttf-dejavu && rm -rf /var/cache/apk/*
在应用的启动参数, 设置 -XX:+UseContainerSupport, 设置 - XX:MaxRAMPercentage=75.0, 这样为其他进程 (debug, 监控) 留下足够的内存空间, 又不会太浪费 RAM.
作者: Jadepeng
出处: jqpeng 的技术记事本 --http://www.cnblogs.com/xiaoqi
您的支持是对博主最大的鼓励, 感谢您的认真阅读.
来源: https://www.cnblogs.com/xiaoqi/p/container-jvm.html