一次 Java 虚拟机启动失败问题排查:Cannot create GC thread 背后的 glibc / clone3 兼容性
一次 Java 虚拟机启动失败问题的排查:Cannot create GC thread 背后的 glibc / clone3 兼容性
背景
某流式计算任务在一类基于旧版 Docker 运行时的容器环境中启动失败,报错如下:
1
2
There is insufficient memory for the Java Runtime Environment to continue.
Cannot create GC thread. Out of system resources.
表面上看,这像是“系统资源不足”导致 JVM 无法启动;但进一步排查后发现,真实原因并不是物理机内存不足,也不是 JDK 版本本身的问题,而是:
- 新基础镜像中的
glibc版本较新; - 新版
glibc默认优先尝试clone3()系统调用来创建线程; - 旧版 Docker /
seccomp策略没有正确支持clone3; - 导致线程创建失败,最终 JVM 无法创建 GC 线程并启动失败。
这类问题很容易被误判为“内存不够”或“JDK 有问题”,因此很值得单独记录。
现象
任务启动阶段直接失败,JVM 输出:
1
2
There is insufficient memory for the Java Runtime Environment to continue.
Cannot create GC thread. Out of system resources.
从字面看,这会让人优先怀疑以下几个方向:
- 宿主机内存、线程数或其他系统资源不足;
- 某个特定 JDK 版本存在兼容性问题;
- 容器镜像或运行时环境发生了变化。
初步排查:几个容易走偏的方向
1. 误以为是物理机资源不足
JVM 明确报出 Out of system resources,第一反应通常是宿主机资源不足。但当同样的问题在多台机器上稳定复现时,这个解释就站不住了:
- 如果是单机资源偶发不足,通常不会在多台机器上表现一致;
- 问题具备环境稳定复现特征,更像是镜像或运行时兼容性问题。
因此,“机器资源真的不够”这个方向可以较早排除。
2. 怀疑是 JDK 版本问题
由于现场使用的 JDK 版本与历史常用版本不同,也一度怀疑是 JDK 版本升级带来的兼容性问题。
但在替换为另一套 JDK 基础镜像后,问题依然存在,说明:
- 这不是某个特定 JDK 版本独有的问题;
- 更可能是 JDK 所运行的容器基础环境存在系统级兼容性问题。
3. 镜像方向逐渐浮出水面
当“物理资源不足”和“JDK 版本问题”两个方向都被排除后,排查重点自然转向基础镜像。
进一步确认后发现,失败任务使用的是基于较新 Linux 发行版构建的基础镜像;而问题正是从这里开始的。
关键转折:社区已有类似案例
检索公开资料后,可以找到与该现象高度一致的社区讨论,例如:
- Adoptium / Temurin 容器相关 issue
- Moby(Docker Engine)关于
clone3与seccomp的修复提交
这些讨论给出的核心结论非常一致:
- 较新的基础镜像会带来较新的
glibc; - 较新的
glibc会默认尝试使用clone3(); - 老版本 Docker 默认
seccomp策略对clone3处理不正确; - 从而导致线程创建失败;
- Java 进程会在 very early startup 阶段失败,并表现为“无法创建 GC 线程”。
这时问题就从“JVM 看起来像内存不足”转成了“系统调用兼容性问题”。
根因分析
glibc 为什么会影响 JVM 创建线程?
JVM 启动时需要创建多个线程,例如:
- GC 线程
- VM 线程
- 编译线程
- 其他内部工作线程
在线程创建过程中,底层最终会走到 libc 与内核系统调用层。
较新的 glibc 在某些发行版中会优先尝试 clone3()。它的行为大致是:
- 如果
clone3()返回ENOSYS(系统不支持),则回退到传统的clone(); - 如果返回的是其他错误(例如
EPERM),则认为这是致命失败,不再回退。
问题恰恰出在旧版 Docker 默认 seccomp 策略这里:
- 它没有正确为
clone3定义规则; - 某些情况下返回的不是可回退的
ENOSYS,而是EPERM; - 于是
glibc不会再回退到clone(); - 线程创建直接失败;
- JVM 无法继续启动,于是报出
Cannot create GC thread。
为什么报错看起来像“内存不足”?
因为 JVM 在启动阶段并不能感知“真正的业务语义原因”,它只知道:
- 关键线程创建失败了;
- 当前运行时环境无法提供继续执行所需的系统资源。
于是最终表现为一种比较泛化的错误描述:
1
Out of system resources
但这里的“system resources”并不一定是狭义的内存,也可能是线程创建相关的系统调用能力或安全策略兼容性。
具体验证
为了验证判断,可以直接切换基础镜像版本进行对比。
实验现象如下:
- 使用基于 Debian 11 的基础镜像时,任务能够正常启动;
- 使用基于 Debian 12 的基础镜像时,任务启动失败。
进一步对比 glibc 版本:
- Debian 11:
glibc 2.31 - Debian 12:
glibc 2.36
这与社区问题中的分界点是吻合的:
- 较新的
glibc会优先使用clone3; - 在旧版 Docker 环境下会触发兼容性问题;
- 较老版本
glibc不会走到这条失败路径,因此表现正常。
这基本可以确认,问题不是 Java 本身的逻辑错误,而是:
新基础镜像 + 新版
glibc+ 旧版 Docker /seccomp策略 = JVM 启动失败
为什么另一些容器环境没有问题?
同一个任务在另一类基于 Kubernetes 的容器环境中可以正常运行。
这说明问题并不在应用本身,而在于底层容器运行时实现差异:
- 某些环境并不依赖这套旧版 Docker 运行时路径;
- 或其底层运行时 / 安全策略已经正确处理了
clone3; - 因此不会触发线程创建失败。
换句话说,这是一个特定运行时环境组合下的兼容性问题,而不是“只要升级基础镜像 JVM 就一定挂”。
解决方案
根本方案
升级 Docker Engine / 容器运行时,使其默认 seccomp 策略能够正确处理 clone3。
这是最彻底的方案,因为随着发行版持续升级,未来越来越多的新基础镜像都会依赖新版 glibc,问题不会只停留在某一个镜像版本。
临时规避方案
如果短期内无法升级容器运行时,可以选择:
- 回退基础镜像到较老发行版,例如 Debian 11;
- 自定义允许
clone3的seccomp策略; - 在受控范围内,避免直接切到依赖新版
glibc的基础镜像。
在实际生产环境中,最容易落地的往往是第一种:
- 先把基础镜像回退到 Debian 11;
- 待底层运行时升级完成后,再推进到更新的发行版。
影响范围
这类问题通常会命中如下组合:
- Java 服务 / Java 计算任务
- 使用较新的基础镜像(例如 Debian 12 / Ubuntu 22.04 一类)
- 运行在较老版本 Docker 引擎或旧
seccomp策略环境中
而在以下环境中,可能不受影响:
- 已升级容器运行时的环境
- 不依赖旧版 Docker
seccomp默认行为的环境 - 仍使用较老发行版基础镜像的环境
因此,真正需要关注的不是“JDK 某个小版本”,而是:
基础镜像版本、
glibc版本、容器运行时版本、seccomp策略版本是否匹配。
排查经验总结
这个案例里最容易踩的坑有两个:
1. 被 JVM 报错字面意思带偏
Cannot create GC thread + Out of system resources 很容易让人立刻去看:
- 内存是不是不够;
- ulimit 是否太小;
- 线程数是否达到上限。
这些方向并非完全错误,但如果问题具有“跨机器稳定复现、切镜像后行为变化明显”的特征,就应该尽快把关注点转向系统兼容性。
2. 低估基础镜像升级带来的系统级变化
很多时候大家会把镜像升级理解为“换了个 OS 版本”,但实际上这可能意味着:
glibc版本变化;- 默认系统调用路径变化;
- 安全策略兼容性变化;
- 线程、进程创建行为变化。
对于 Java 这种高度依赖底层线程能力的运行时来说,这类变化完全可能表现为 JVM 启动失败。
一句话结论
这次问题的根因不是内存不足,也不是某个 JDK 版本 bug,而是:
新版基础镜像中的
glibc默认使用clone3(),而旧版 Docker /seccomp环境无法正确兼容,导致 JVM 无法创建线程并启动失败。
如果你的 Java 应用在升级基础镜像后突然出现:
1
2
Cannot create GC thread
Out of system resources
请不要只盯着内存,优先检查:
- 基础镜像是否升级到了更新发行版;
glibc是否变新;- Docker Engine / 容器运行时是否较老;
seccomp是否允许或正确处理clone3。
参考资料
- Adoptium / Temurin 容器问题讨论:Java 在线程创建阶段失败与基础镜像升级相关
- Moby(Docker Engine)关于
clone3的seccomp修复提交 glibc对clone3()与clone()回退逻辑的相关说明