外观
容器里的 Java:内存、信号与堆转储
同一份 JVM 参数,在物理机上没问题,进了容器就 OOMKilled。原因是 JVM 看到的内存和 cgroup 限制不是一回事,以及容器里的 PID 1 信号处理不同。
一、JVM 到底看到多少内存
bash
docker run -it --memory 2g eclipse-temurin:17-jre \
java -XX:+PrintFlagsFinal -version | grep -E 'MaxHeapSize|ActiveProcessorCount'text
size_t MaxHeapSize = 536870912 {product} {ergonomic}
uint ActiveProcessorCount = 2 {product} {ergonomic}2g 限制下,MaxHeapSize 是 512MB(物理机的 1/4 规则被换成了容器 limit 的 1/4)。
前提:JDK 10+ 默认开启 UseContainerSupport;JDK 8 需要 8u191+,否则它会读宿主机内存。
bash
# 确认容器支持是否生效
java -XshowSettings:system -version 2>&1 | head -20text
Operating System Metrics:
Provider: cgroupv2
Effective CPU Count: 2
CPU Period: 100000us
CPU Quota: 200000us
CPU Shares: 1024
List of Processors: N/A
List of Effective Processors: N/A
List of Memory Nodes: N/A
Available Physical Memory: 2147483648Effective CPU Count: 2、Available Physical Memory: 2G —— cgroup 感知生效。
二、堆大小怎么配
| 方式 | 写法 | 说明 |
|---|---|---|
| 固定堆 | -Xms2g -Xmx2g | 最可预测,但改了 limit 要改参数 |
| 百分比 | -XX:MaxRAMPercentage=70 | 按容器 limit 自动算 |
| 初始比例 | -XX:InitialRAMPercentage=70 | 通常与上面设成一样,避免扩容 |
bash
java -XX:InitialRAMPercentage=70 -XX:MaxRAMPercentage=70 \
-XX:MaxMetaspaceSize=256m -XX:MaxDirectMemorySize=256m \
-Xss512k -jar app.jar容器 limit = Xmx 必被 OOMKilled
Java 进程占用 = 堆 + 元空间 + 线程栈 + 直接内存 + JIT 代码缓存 + GC 结构 + JNI。
limit 2g + -Xmx2g 时,堆还没满,进程 RSS 已经超过 2g,被内核杀掉。留 30% 余量:
text
limit 2g → 堆 ~1.4g还有一种更隐蔽的情况:没有设置 MaxDirectMemorySize,Netty/NIO 分配的直接内存不计入堆,但计入 RSS。堆设 70% 时,剩下 30% 要同时容纳元空间、线程栈和直接内存,重负载下仍可能超。
三、被 OOMKilled 的确认
bash
docker inspect -f '{{.State.OOMKilled}} {{.State.ExitCode}}' order-apitext
true 137bash
dmesg -T | grep -i 'memory cgroup out of memory' | tail -3text
[Wed Oct 28 03:04:11 2026] Memory cgroup out of memory: Killed process 41200 (java)
total-vm:8421412kB, anon-rss:6294528kB容器内看当前用量:
bash
docker exec order-api cat /sys/fs/cgroup/memory.current
docker exec order-api cat /sys/fs/cgroup/memory.max
docker exec order-api cat /sys/fs/cgroup/memory.eventstext
6294528000
6442450944
low 0
high 0
max 12
oom 2
oom_kill 2oom_kill 2 是直接证据。
四、在容器里抓堆转储
容器删了 dump 就没了,必须挂载目录。
bash
docker run -d --name order-api \
--memory 2g \
-e JAVA_OPTS="-XX:MaxRAMPercentage=70 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dumps" \
-v /data/order-api/dumps:/dumps \
myapp:1.4.2bash
# 手动抓(会 STW,生产慎用)
docker exec order-api jcmd 1 GC.heap_dump /dumps/manual.hprof
# 看堆概况(轻量)
docker exec order-api jcmd 1 GC.heap_infotext
PSYoungGen total 524288K, used 89124K
ParOldGen total 1048576K, used 1048576K
Metaspace used 148231K, committed 165888KParOldGen 满了且 used == total,说明老年代撑满,可能内存泄漏。
hprof 文件和内存一样大
2G 堆的 dump 约 2G 磁盘。容器磁盘紧张时,dump 本身会把磁盘写满,导致新的问题。挂载到独立盘,并限制数量。
五、优雅停机
容器里 PID 1 默认是 Dockerfile 的 ENTRYPOINT。如果写成 shell 形式:
dockerfile
CMD java -jar /app/app.jar实际 PID 1 是 /bin/sh -c java ...,SIGTERM 发给 sh,不会转发给 java,10 秒后 SIGKILL。
验证:
bash
docker exec order-api ps -o pid,ppid,args -p 1text
PID PPID ARGS
1 0 /bin/sh -c java -jar /app/app.jar修法:
dockerfile
ENTRYPOINT ["java", "-jar", "/app/app.jar"]bash
docker exec order-api ps -o pid,ppid,args -p 1text
PID PPID ARGS
1 0 java -jar /app/app.jar应用侧配合:
yaml
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 25sbash
docker stop -t 30 order-api-t 30 要大于优雅停机时间(25s),否则 Docker 提前 SIGKILL。
六、CPU 限制的影响
bash
docker run --cpus 2 ...JVM 看到 ActiveProcessorCount=2,于是:
- GC 线程数、JIT 编译线程数按 2 核算。
ForkJoinPool.commonPool并行度变成 1。
如果 --cpus 设得太小(如 0.5),GC 线程会被严重限制,表现为 GC 变慢、延迟升高。
bash
# 显式指定(不建议,让 JVM 自己算更好)
-XX:ActiveProcessorCount=2七、时区与字符集
bash
docker exec order-api date
docker exec order-api localetext
Wed Oct 28 03:14:12 UTC 2026
LANG=
LC_ALL=容器默认 UTC + 无 locale。修法:
dockerfile
ENV TZ=Asia/Shanghai \
LANG=C.UTF-8 \
LC_ALL=C.UTF-8Alpine 还需要装 tzdata:
dockerfile
RUN apk add --no-cache tzdata && \
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo "Asia/Shanghai" > /etc/timezone否则 TZ 设了,date 还是 UTC(musl 没有内置时区数据)。
八、一份可用的启动参数
bash
java \
-XX:InitialRAMPercentage=70 \
-XX:MaxRAMPercentage=70 \
-XX:MaxMetaspaceSize=256m \
-XX:MaxDirectMemorySize=256m \
-Xss512k \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-Xlog:gc*:file=/app/logs/gc.log:time,uptime:filecount=5,filesize=50m \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/dumps \
-Duser.timezone=Asia/Shanghai \
-Dfile.encoding=UTF-8 \
-jar /app/app.jar对应的 Docker 运行参数:
bash
docker run -d --name order-api \
--memory 2g --memory-swap 2g --cpus 2 \
--restart unless-stopped \
-p 127.0.0.1:8080:8080 \
-v /data/order-api/logs:/app/logs \
-v /data/order-api/dumps:/dumps \
-e TZ=Asia/Shanghai \
order-api:1.4.2九、踩过的坑
| 现象 | 根因 | 处置 |
|---|---|---|
| 容器 OOMKilled 但堆没满 | 堆外内存超限 | 留 30% 余量,限制直接内存 |
| JVM 按宿主机内存算堆 | JDK 8u191 以下 / 关了 UseContainerSupport | 升级 JDK 或加参数 |
docker stop 要等 10 秒 | shell 形式 ENTRYPOINT,信号不转发 | 改 exec 形式 |
| OOM 后没有 dump | dump 路径在容器内,容器已删 | 挂载目录 |
| dump 把磁盘写满 | hprof 和堆一样大 | 独立盘 + 清理 |
| GC 很慢 | --cpus 太小,GC 线程受限 | 提高 CPU 配额 |
| 时间差 8 小时 | 容器 UTC | TZ + tzdata |
| 中文乱码 | 无 locale | LANG=C.UTF-8 |