Skip to content

容器里的 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 -20
text
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: 2147483648

Effective 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-api
text
true 137
bash
dmesg -T | grep -i 'memory cgroup out of memory' | tail -3
text
[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.events
text
6294528000
6442450944
low 0
high 0
max 12
oom 2
oom_kill 2

oom_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.2
bash
# 手动抓(会 STW,生产慎用)
docker exec order-api jcmd 1 GC.heap_dump /dumps/manual.hprof

# 看堆概况(轻量)
docker exec order-api jcmd 1 GC.heap_info
text
 PSYoungGen      total 524288K, used 89124K
 ParOldGen       total 1048576K, used 1048576K
 Metaspace       used 148231K, committed 165888K

ParOldGen 满了且 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 1
text
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 1
text
PID   PPID  ARGS
    1     0  java -jar /app/app.jar

应用侧配合:

yaml
server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 25s
bash
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 locale
text
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-8

Alpine 还需要装 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 后没有 dumpdump 路径在容器内,容器已删挂载目录
dump 把磁盘写满hprof 和堆一样大独立盘 + 清理
GC 很慢--cpus 太小,GC 线程受限提高 CPU 配额
时间差 8 小时容器 UTCTZ + tzdata
中文乱码无 localeLANG=C.UTF-8