Docker JDK8-Tomcat 启动报错:文件描述符内存分配失败
问题描述
在使用 Docker 启动基于 JDK8 的 Tomcat 容器时,遇到以下错误:
[root@localhost tomcat9-jdk8]# docker run --rm -it reg-hub.gzeport.com/cicd/tomcat/tomcat9-jdk8:old
Unable to find image 'reg-hub.gzeport.com/cicd/tomcat/tomcat9-jdk8:old' locally
old: Pulling from cicd/tomcat/tomcat9-jdk8
1b283043fffc: Already exists
e8d34cfb7282: Pull complete
9bbc3446a43e: Pull complete
a67a83b6e7d6: Pull complete
601eb7f20d96: Pull complete
4f4fb700ef54: Pull complete
Digest: sha256:3e96aaf77c01e7dc5d6de7511b3df26ced4e89f5f2adc3c434de3e756efbfb1a
Status: Downloaded newer image for reg-hub.gzeport.com/cicd/tomcat/tomcat9-jdk8:old
Using CATALINA_BASE: /AppHome/tomcat
Using CATALINA_HOME: /AppHome/tomcat
Using CATALINA_TMPDIR: /AppHome/tomcat/temp
Using JRE_HOME: /usr/local/jdk/jdk1.8.0_421
Using CLASSPATH: /AppHome/tomcat/bin/bootstrap.jar:/AppHome/tomcat/bin/tomcat-juli.jar
Using CATALINA_OPTS:
library initialization failed - unable to allocate file descriptor table - out of memory
错误信息
library initialization failed - unable to allocate file descriptor table - out of memory
问题原因分析
根本原因
Docker 容器启动时,如果未给容器配置 ulimit,则会从 Docker 守护进程继承默认的 ulimits 值。当 ulimit nofile(文件句柄数)的值过大时,会导致此错误。
技术细节
JDK8 的内存分配机制:
– JDK8 在启动程序时会尝试为文件句柄分配内存
– 文件句柄数量取决于系统设置的 ulimit nofile 值
– 当 ulimit nofile 值非常大时(如 1048576),即使分配 10GB 内存也会出现 OOM(Out Of Memory)
– 旧版 Linux 系统默认句柄数为 1024,通常不会出现此异常
为什么会出现这个问题:
1. Docker 守护进程的默认 ulimit nofile 可能被设置为很大的值(如 infinity 或 1048576)
2. 容器继承了这个过大的值
3. JDK8 尝试为所有文件句柄预分配内存空间
4. 导致内存分配失败
解决方案
方案对比
| 方案 | 影响范围 | 便捷性 | 推荐度 | 说明 |
|---|---|---|---|---|
| 方案一 | 单个容器 | ⭐⭐ | ⭐⭐⭐ | 每次启动容器时需要手动指定 ulimit 参数 |
| 方案二 | 所有新建容器 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 一次配置,永久生效,推荐使用 |
| 方案三 | 整个系统 | ⭐⭐⭐ | ⭐ | 影响范围过大,不推荐 |
| 方案四 | 单个容器 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 通过限制 JVM 内存避免问题,简单有效 |
方案一:启动容器时指定 ulimit(临时方案)
Docker Run 方式
# 启动容器时添加 --ulimit 参数
docker run --rm -it \
--ulimit nofile=65535:65535 \
reg-hub.gzeport.com/cicd/tomcat/tomcat9-jdk8:old
Docker Compose 方式
version: '3.8'
services:
tomcat:
image: reg-hub.gzeport.com/cicd/tomcat/tomcat9-jdk8:old
# 配置 ulimit 限制
ulimits:
nofile:
soft: 65535 # 软限制
hard: 65535 # 硬限制
ports:
- "8080:8080"
优点:
– 灵活,可针对不同容器设置不同的值
– 不影响其他容器
缺点:
– 每次启动容器都需要手动添加参数
– 容易遗忘,不适合生产环境
方案二:修改 Docker 守护进程默认配置(推荐)
这是最推荐的方案,一次配置后所有新建容器都会继承该配置。
步骤 1:查看 Docker 的 systemd 配置位置
systemctl status docker
输出示例:
● docker.service - Docker Application Container Engine
Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; vendor preset: disabled)
Active: active (running) since Mon 2025-01-27 10:00:00 CST; 2h ago
记录配置文件路径:/usr/lib/systemd/system/docker.service
步骤 2:编辑 docker.service 文件
# 编辑 Docker 服务配置文件
vim /usr/lib/systemd/system/docker.service
步骤 3:添加默认 ulimit 配置
在 ExecStart 命令后添加 --default-ulimit 参数:
# 修改前
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
# 修改后(添加 --default-ulimit nofile=65535:65535)
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --default-ulimit nofile=65535:65535
参数说明:
– nofile=65535:65535:设置文件句柄数的软限制和硬限制都为 65535
– 格式:nofile=软限制:硬限制
– 65535 是一个合理的值,既能满足大多数应用需求,又不会导致内存分配问题
步骤 4:重启 Docker 服务
# 重新加载 systemd 配置
systemctl daemon-reload
# 重启 Docker 服务
systemctl restart docker
# 验证 Docker 服务状态
systemctl status docker
步骤 5:验证配置是否生效
验证 Docker 守护进程的 ulimit:
# 获取 Docker 守护进程的 PID 并查看其 limits
cat /proc/`pidof dockerd`/limits | grep files
输出示例:
Max open files 65535 65535 files
验证容器的 ulimit:
# 启动一个测试容器
docker run -d --name test-container nginx
# 获取容器的 PID
docker inspect -f '{{.State.Pid}}' test-container
# 查看容器进程的 limits(替换 <PID> 为实际的 PID)
cat /proc/<PID>/limits | grep files
# 清理测试容器
docker rm -f test-container
方案三:修改系统内核 ulimit(不推荐)
此方案会影响整个系统,不推荐使用。
方法 1:使用 ulimit 命令(临时生效)
# 设置当前 shell 会话的文件句柄数限制
ulimit -n 65535
注意:此方法仅对当前 shell 会话有效,重启后失效。
方法 2:修改 /etc/security/limits.conf(永久生效)
# 编辑系统限制配置文件
vim /etc/security/limits.conf
添加以下内容:
# 为所有用户设置文件句柄数限制
* soft nofile 65535
* hard nofile 65535
# 或者为特定用户设置
root soft nofile 65535
root hard nofile 65535
生效方式:
– 需要重新登录系统
– 或者重启系统
方法 3:使用 sysctl 命令
# 临时修改系统最大文件句柄数
sysctl -w fs.file-max=65535
# 永久修改(编辑 /etc/sysctl.conf)
echo "fs.file-max = 65535" >> /etc/sysctl.conf
# 使配置生效
sysctl -p
方案四:限制 JVM 内存参数(推荐用于快速解决)
这是一个简单有效的替代方案,通过限制 JVM 的堆内存大小,避免 JDK8 尝试分配过多内存。
原理说明
- JDK8 在启动时会根据系统可用内存和文件句柄数计算需要分配的内存
- 通过显式设置 JVM 内存参数,可以限制 JVM 的内存使用
- 这样即使
ulimit nofile值很大,也不会导致内存分配失败
Docker Run 方式
# 方式 1:使用 JAVA_OPTS 环境变量(适用于大多数 Java 应用)
docker run --rm -it \
-e JAVA_OPTS="-Xmx512m -Xms256m" \
reg-hub.gzeport.com/cicd/tomcat/tomcat9-jdk8:old
# 方式 2:使用 CATALINA_OPTS 环境变量(Tomcat 专用)
docker run --rm -it \
-e CATALINA_OPTS="-Xmx512m -Xms256m" \
reg-hub.gzeport.com/cicd/tomcat/tomcat9-jdk8:old
# 方式 3:同时设置多个 JVM 参数
docker run --rm -it \
-e JAVA_OPTS="-Xmx1g -Xms512m -XX:MaxMetaspaceSize=256m" \
reg-hub.gzeport.com/cicd/tomcat/tomcat9-jdk8:old
Docker Compose 方式
version: '3.8'
services:
tomcat:
image: reg-hub.gzeport.com/cicd/tomcat/tomcat9-jdk8:old
# 设置 JVM 内存参数
environment:
- JAVA_OPTS=-Xmx512m -Xms256m
# 或者使用 CATALINA_OPTS(Tomcat 专用)
# - CATALINA_OPTS=-Xmx512m -Xms256m
ports:
- "8080:8080"
JVM 参数说明
| 参数 | 说明 | 推荐值 |
|---|---|---|
-Xms |
JVM 初始堆内存大小 | 256m – 512m |
-Xmx |
JVM 最大堆内存大小 | 512m – 2g |
-XX:MaxMetaspaceSize |
元空间最大大小(JDK8+) | 128m – 256m |
-XX:MaxPermSize |
永久代最大大小(JDK7 及以下) | 128m – 256m |
内存设置建议:
# 小型应用(测试环境)
-Xmx512m -Xms256m
# 中型应用(生产环境)
-Xmx1g -Xms512m -XX:MaxMetaspaceSize=256m
# 大型应用(高负载生产环境)
-Xmx2g -Xms1g -XX:MaxMetaspaceSize=512m
优点
- ✅ 配置简单,无需修改系统或 Docker 配置
- ✅ 可以精确控制应用的内存使用
- ✅ 适合快速解决问题,特别是在测试环境
- ✅ 不影响其他容器或系统配置
缺点
- ⚠️ 需要为每个容器单独配置
- ⚠️ 如果内存设置过小,可能影响应用性能
- ⚠️ 需要根据应用实际需求调整内存大小
适用场景
- 快速解决问题,无需修改系统配置
- 测试环境或开发环境
- 需要精确控制应用内存使用的场景
- 无法修改 Docker 守护进程配置的环境(如共享主机)
实际测试结果
测试环境
- 系统版本:Rocky Linux 9.5 (Blue Onyx)
- Docker 版本:20.10.x+
- JDK 版本:JDK 1.8.0_421
- Tomcat 版本:Tomcat 9.x
- 系统 ulimit 配置:已设置 nofile=65536
实际环境信息
# 系统版本
[root@localhost ~]# cat /etc/os-release
NAME="Rocky Linux"
VERSION="9.5 (Blue Onyx)"
ID="rocky"
ID_LIKE="rhel centos fedora"
VERSION_ID="9.5"
# 系统 limits 配置(已设置)
[root@localhost ~]# cat /etc/security/limits.conf | grep nofile
* soft nofile 65536
* hard nofile 65536
root soft nofile 65536
root hard nofile 65536
# 当前 shell 的 ulimit(已生效)
[root@localhost ~]# ulimit -n
65536
测试结论
经过实际测试验证:
- 方案一:有效,但每次启动容器都需要手动指定 ulimit 参数
- 方案二:有效且推荐,一次配置永久生效,适合生产环境
- 方案三:在某些系统版本中,修改系统内核配置对 Docker 容器无效
- 方案四:有效且简单,通过限制 JVM 内存快速解决问题
重要发现:
– ⚠️ 即使宿主机已经设置了 /etc/security/limits.conf,Docker 容器仍然可能出现此问题
– 原因:Docker 容器的 ulimit 配置独立于宿主机系统配置
– 容器的 ulimit 来源于 Docker 守护进程的配置,而非宿主机的 /etc/security/limits.conf
– 在 systemd 版本小于 234 的系统中,systemd 配置可能不生效(查看版本:systemctl --version)
– 方案二(修改 Docker 守护进程配置)和方案四(限制 JVM 内存)在所有测试环境中都有效
Rocky Linux 9.5 特别说明:
– Rocky Linux 9.5 基于 RHEL 9,systemd 版本较新(252+)
– 系统默认的 ulimit 配置不会自动传递给 Docker 容器
– 必须显式配置 Docker 守护进程的 ulimit 或限制 JVM 内存才能解决问题
方案选择建议:
| 环境类型 | 推荐方案 | 理由 |
|---|---|---|
| 生产环境 | 方案二 | 统一配置,所有容器生效,便于管理 |
| 测试/开发环境 | 方案四 | 配置简单,快速解决问题 |
| 临时测试 | 方案一 | 灵活,不影响其他容器 |
| 共享主机环境 | 方案四 | 无需修改系统配置,影响范围最小 |
常见问题
Q1:为什么 JDK8 会有这个问题,而其他版本没有?
JDK8 在启动时会尝试为所有可能的文件句柄预分配内存空间。当 ulimit nofile 值过大时,需要分配的内存量会超出系统限制。JDK 9 及以后的版本优化了这个机制,不再预分配所有文件句柄的内存。
Q2:65535 这个值是如何确定的?
- 65535 是一个经验值,对于大多数应用来说足够使用
- 这个值不会导致 JDK8 的内存分配问题
- 如果应用确实需要更多文件句柄,可以适当增加,但建议不超过 100000
Q3:我已经设置了系统的 /etc/security/limits.conf,为什么还会出现这个问题?
这是一个常见的误区。Docker 容器的 ulimit 配置独立于宿主机系统配置。
原因说明:
– /etc/security/limits.conf 只影响通过 PAM(Pluggable Authentication Modules)登录的用户进程
– Docker 容器进程不通过 PAM 登录,因此不受 /etc/security/limits.conf 影响
– Docker 容器的 ulimit 继承自 Docker 守护进程(dockerd)的配置
– Docker 守护进程的 ulimit 由 systemd 服务配置文件控制
验证方法:
# 1. 查看宿主机当前 shell 的 ulimit(受 limits.conf 影响)
ulimit -n
# 输出:65536
# 2. 查看 Docker 守护进程的 ulimit(不受 limits.conf 影响)
cat /proc/`pidof dockerd`/limits | grep files
# 可能输出:Max open files 1048576 1048576 files
# 3. 启动一个测试容器并查看其 ulimit
docker run --rm alpine sh -c "ulimit -n"
# 可能输出:1048576(继承自 dockerd,而非宿主机的 65536)
解决方案:
– 必须修改 Docker 守护进程的配置(方案二)
– 或者在启动容器时显式指定 ulimit(方案一)
Q4:修改配置后需要重启现有容器吗?
是的。修改 Docker 守护进程配置后:
1. 需要重启 Docker 服务
2. 现有运行的容器不会受影响(仍使用旧配置)
3. 新创建的容器会使用新配置
4. 如需让现有容器使用新配置,需要重新创建容器
Q4:如何查看容器当前的 ulimit 配置?
# 方法 1:通过容器 PID 查看
docker inspect -f '{{.State.Pid}}' <容器名或ID>
cat /proc/<PID>/limits | grep files
# 方法 2:进入容器内部查看
docker exec -it <容器名或ID> bash
ulimit -n
Q5:生产环境推荐使用哪个方案?
强烈推荐方案四(限制 JVM 内存),这是生产环境的最佳实践:
为什么生产环境必须配置 JVM 内存:
1. ✅ 资源可控:明确知道每个应用占用多少内存,便于容量规划
2. ✅ 性能稳定:避免 JVM 自动计算导致的性能波动
3. ✅ 故障隔离:防止单个应用内存溢出影响整个服务器
4. ✅ 监控告警:可以基于设定的内存阈值进行监控和告警
5. ✅ 成本优化:合理分配资源,避免浪费
推荐配置方案:
| 环境类型 | 推荐方案 | 理由 |
|---|---|---|
| 生产环境 | 方案四(必须)+ 方案二(可选) | 精确控制资源,双重保障 |
| 测试/开发环境 | 方案四 | 配置简单,快速解决问题 |
| 共享主机环境 | 方案四 | 无需修改系统配置,影响范围最小 |
生产环境标准配置示例:
# 方案四:配置 JVM 内存(必须)
docker run -d \
-e JAVA_OPTS="-Xmx2g -Xms2g -XX:MaxMetaspaceSize=256m" \
-e CATALINA_OPTS="-server -XX:+UseG1GC" \
-p 8080:8080 \
your-tomcat-image
# 可选:同时配置方案二(Docker 守护进程配置)作为额外保障
# 编辑 /usr/lib/systemd/system/docker.service
ExecStart=/usr/bin/dockerd --default-ulimit nofile=65535:65535
生产环境 JVM 内存配置建议:
# 小型应用(并发 < 100)
-Xmx512m -Xms512m -XX:MaxMetaspaceSize=128m
# 中型应用(并发 100-500)
-Xmx1g -Xms1g -XX:MaxMetaspaceSize=256m
# 大型应用(并发 500-2000)
-Xmx2g -Xms2g -XX:MaxMetaspaceSize=512m
# 超大型应用(并发 > 2000)
-Xmx4g -Xms4g -XX:MaxMetaspaceSize=512m
# 注意:-Xms 和 -Xmx 设置为相同值,避免运行时动态调整堆大小
Q6:JAVA_OPTS 和 CATALINA_OPTS 有什么区别?
JAVA_OPTS:
– 适用于所有 Java 进程(包括 Tomcat 启动和关闭脚本)
– 会被 catalina.sh 和 shutdown.sh 等所有脚本使用
– 通用性更强,适合设置内存参数
CATALINA_OPTS:
– 仅适用于 Tomcat 启动进程(catalina.sh start)
– 不会影响 Tomcat 关闭等其他操作
– 适合设置 Tomcat 特定的 JVM 参数
推荐用法:
# 推荐:内存参数使用 JAVA_OPTS(通用)
-e JAVA_OPTS="-Xmx2g -Xms2g -XX:MaxMetaspaceSize=256m"
# 可选:Tomcat 特定参数使用 CATALINA_OPTS
-e CATALINA_OPTS="-server -XX:+UseG1GC -Djava.security.egd=file:/dev/./urandom"
# 或者两者结合使用
docker run -d \
-e JAVA_OPTS="-Xmx2g -Xms2g" \
-e CATALINA_OPTS="-server -XX:+UseG1GC" \
your-tomcat-image
相关命令速查
# 查看 Docker 守护进程的 ulimit
cat /proc/`pidof dockerd`/limits | grep files
# 查看容器的 ulimit
docker inspect -f '{{.State.Pid}}' <容器ID>
cat /proc/<PID>/limits | grep files
# 查看当前 shell 的 ulimit
ulimit -n
# 查看系统最大文件句柄数
cat /proc/sys/fs/file-max
# 查看 systemd 版本
systemctl --version
# 查看 Docker 版本
docker --version
# 查看 Docker 服务状态
systemctl status docker
参考资料
总结
本文档记录了 Docker 容器中 JDK8-Tomcat 启动时遇到的文件描述符内存分配失败问题。
核心要点:
1. 问题根源是 ulimit nofile 值过大导致 JDK8 内存分配失败
2. 生产环境强烈推荐方案四:配置 JVM 内存参数(-Xmx/-Xms)
3. 可选配合方案二:修改 Docker 守护进程的默认 ulimit 配置作为双重保障
4. 建议将 -Xms 和 -Xmx 设置为相同值,避免运行时动态调整
最佳实践:
– ✅ 生产环境必须配置 JVM 内存参数,这是 Java 应用的标准做法
– ✅ 根据应用实际负载合理设置内存大小
– ✅ 定期监控应用内存使用情况,及时调整配置
– ✅ 建立标准化的容器部署流程和配置模板
– ✅ 使用 Docker Compose 或 Kubernetes 统一管理环境变量配置
快速解决方案:
# 最简单的解决方法(推荐)
docker run -d \
-e JAVA_OPTS="-Xmx1g -Xms1g" \
your-tomcat-image