2487 字
约 8 分钟
18
S05-A18 · 线程起不来:`OutOfMemoryError: unable to create native thread`
2024-12-31
2026-11-10

一个服务在压力上来后报 unable to create new native thread,而堆内存只用了一半。运维加了堆,问题更严重了——因为这个错误与堆无关,它报的是"线程栈或系统资源不够",这个"反而"有个直白的机制:容器或机器的总内存固定,堆占得越多,留给栈与直接内存的部分越少。所以"加堆治线程问题"是把原因之一当成了解药。
而堆越大,可用于线程栈的空间反而越少。

线程数上限是 JVM 里唯一同时受"JVM 参数、操作系统、容器限制"三者约束的资源。任何一处到达上限,表现都是同一个异常,但修法完全不同。


1. 四个上限,谁先到

上限 来源
堆外可用内存 / 线程栈大小 JVM 参数与物理内存
ulimit -u(进程数) 操作系统
kernel.pid_max / cgroup pids 系统 / 容器
线程池的自身容量 应用配置

第一项最容易被误解:-Xss 默认 512KB–1MB,1000 个线程就是 0.5–1GB,而这部分内存不在堆里。所以"堆加到 8G"会让线程栈可用的空间更少。

第二项在容器里常被忽略:cgroup 的 pids 限制会让"线程创建"被拒绝,而报错仍然是同一个异常。

2. 报错文案与真实原因

文案 含义
unable to create new native thread 线程栈/进程数/系统资源
OutOfMemoryError: Java heap space 堆不够
OutOfMemoryError: Metaspace 类元数据
pthread_create failed (EAGAIN) 系统级限制(底层日志)

第一项与第二项完全不同,但很多人看到 OutOfMemoryError 就以为是堆问题。区分方式看冒号后面那一段。第二项(堆)与第三项(Metaspace)也有类似错位:Metaspace 不足时加堆无效,而它的表现是"频繁 Full GC 但堆占用不高"。

第三项(Metaspace)的常见触发是"动态生成类":大量反射代理、脚本引擎、热部署都会持续占用 Metaspace,而它默认没有上限(或上限很大),于是"看起来无限制但最终撞墙"。
第四项才是真正的根因信号,而它往往只出现在 stderr 或 GC 日志的尾部。

// 三步定位:线程数、ulimit、容器 pids 限制
jcmd <pid> Thread.print | grep -c '^"'          # 当前线程数
cat /proc/<pid>/limits | grep -i 'processes'    # 进程/线程数上限
cat /sys/fs/cgroup/pids.max 2>/dev/null         # 容器 pids 上限

3. 线程数怎么估

依据 说明
线程池配置之和 确定的数
每请求一线程的框架 并发数相关
连接器线程 Tomcat/Netty worker
定时任务与后台线程 固定但常被忽略

第一项要做的事很简单:把应用里所有线程池的 max 加起来,再加连接器与定时任务。很多人第一次算会发现"配置了 2000 个线程",而机器只允许 1500。

第三项在 -Xss 调大后更吃紧:Tomcat 的 maxThreads 默认 200 不变,但每个线程的栈变大,总占用上升。第四项(定时任务)最容易被算漏:每个定时任务一个线程,加上调度框架自己的线程池,一个"有 20 个定时任务"的服务可能额外占 40–60 个线程。

// 一次真实算账(示例)
tomcat worker   200
业务线程池        8 × 32 = 256
定时任务          16
MQ 消费者         4 × 8 = 32
GC/JIT/其它       ~40
合计              ~544 线程
× 1MB 栈 = 544MB 堆外内存 + ulimit/pids 约束

4. 该调哪一个

修法 代价
减小 -Xss 深递归可能 StackOverflow
增大 ulimit / pids 需容器与宿主配置
减少线程数(改异步) 改动大,但根治
拆分服务 部署变复杂

第一项(减小 -Xss)有个硬下限:递归深度大的代码会先 StackOverflow。所以"减栈"只适合"确认没有深递归"的服务,而这一点很难验证——于是它常被当成最后一招。
第三项最根治:大量线程通常意味着"每个请求占一个线程"的模型,而改成异步(或虚拟线程)能从根上解决。

第四项在"单服务线程太多"时值得:把线程密集的模块拆出去,让上限问题分摊。

// JDK 21:虚拟线程把"线程"从稀缺资源变成廉价的
try (var exec = Executors.newVirtualThreadPerTaskExecutor()) {
    // 可以创建数十万虚拟线程,占用的是堆而不是栈
}
// 前提:阻塞调用需支持虚拟线程(文件 IO 的兼容性需实测)

还有一个"每次请求建线程"的隐性写法,在代码里几乎看不出来:

// 反例:每个任务新建线程(线程数随并发增长,且无法回收)
for (var task : tasks) { new Thread(() -> handle(task)).start(); }

// 正例:固定池 + 队列上限(线程数可控,且有背压)
var pool = new ThreadPoolExecutor(8, 8, 60, SECONDS,
    new ArrayBlockingQueue<>(200), new CallerRunsPolicy());
// CallerRuns 让"队列满"变成"调用方等待",而不是无限建线程

5. 容器里的两个隐形上限

限制 表现
cgroup pids.max 创建失败(同异常)
ulimit -u 继承自宿主/运行时 同上

第一项是容器编排时代的常见坑:默认 pids 限制可能是 512 或 1024,而 JVM 自己就要几十个线程,加上应用很容易撞到。

第二项取决于容器运行时(Docker 默认继承,K8s 可能按配置)。所以"本地好好的,容器里起不来"是典型症状。还有一个"双重限制"的坑:容器 pids 与宿主 ulimit 都可能生效,取更小者。于是"调了容器配置还是不行",因为宿主的限制更低。

// K8s 里显式放开 pids 限制(否则默认值可能很低)
resources:
  limits:
    pid: "4096"        # 或 securityContext 里配
// 检查:cat /sys/fs/cgroup/pids.max

线程数"正常范围"要先建立一个基线,否则看数字无法判断:

// 一个 Java Web 服务的典型线程分布(基线参考)
http-nio-8080-exec   20–200(随并发)
pool-business        8–64(固定上限)
sched-tasks          10–20(定时)
mq-consumer          4–8
JVM 内部(GC/JIT)    20–40
合计                 ~100–350
// 若某天"合计"从 300 涨到 1200 且不回落 → 泄漏,不是容量

6. 排错:五种"线程创建失败"

现象 成因 处理方向
堆只用一半就 OOM 线程栈/系统限制 看报错后缀
加堆后更严重 线程栈空间被挤占 别只加堆
本地正常、容器报错 pids/ulimit 显式配置
压力上来就报 线程池 max 过大 算总账
偶发、重启后正常 泄漏(线程未回收) 查线程增长曲线

第四项(压力上来就报)的判据是"报错是否与并发量线性相关":相关说明线程池 max 太大;不相关说明有别的线程在创建(如每次请求 new Thread)。
第五项最隐蔽:线程泄漏的表现为"线程数随时间单调上升",而它在低峰期完全正常。所以"线程数"应当是一个被监控的指标。

最后一条"选型"边界:线程数与 I/O 模型强相关。同步阻塞模型下"线程数 = 并发数",而异步模型下"线程数 = CPU 核数"。所以同一个容量目标,两种模型的线程需求差两个数量级——这也是"线程上限"问题在异步框架里几乎不存在的原因。

// 同一目标(1 万并发)的两种模型
同步阻塞:约 1 万线程(或排队)→ 栈内存 1 万 × 1MB = 10GB(不可行)
异步/虚拟线程:几十个平台线程(或 1 万虚拟线程,占用在堆)
// 结论:上限问题往往不是"调参",而是"选错了 I/O 模型"

7. 边界:什么情况不必关心线程上限

场景 为什么 做法
小并发内部服务 线程数远低于上限 默认即可
纯异步/事件驱动框架 线程数固定 默认即可
已用虚拟线程 不占栈 关注堆
短生命周期批处理 线程数可控 算一次即可

判断口径一句话:"线程数 × 栈大小"是否接近可用内存,或线程数是否接近进程上限。最后一条与"监控"有关的边界:线程数应当作为一等指标上报(按前缀分组:http-/pool-/sched-)。没有它,"线程泄漏"只能靠事故发现,而它本来可以靠趋势提前两周看见。
接近就该算账;远低于上限,不必操心。


8. 自测

  1. 四个上限分别来自哪里?为什么"加堆"会让线程问题更严重?
  2. 如何从报错文案区分"堆不够"与"线程创建失败"?
  3. 估算线程数要加哪几类?为什么 -Xss 调大后 Tomcat 的默认配置会变紧?
  4. 为什么"改异步/用虚拟线程"是最根治的修法?它的前提是什么?
  5. 容器里两个隐形上限是什么?"本地正常、容器报错"的典型症状指向哪个?
  6. 判断"是否需要关心线程上限"的那个问题是什么?
S05-A18 · 线程起不来:`OutOfMemoryError: unable to create native thread`
https://www.clxhxhhr.top/posts/6524/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。