
一、现象:一个"看起来像内存不足"的假象
某个周一早上,大数据平台的一个 Java 数据处理进程突然疯狂刷异常,任务停摆、接口超时,日志里的报错出奇地一致:
java
java.lang.OutOfMemoryError: Unable to create new native thread
at java.lang.Thread.start0(Native Method)
at java.lang.Thread.start(Thread.java:717)
运维同学的第一反应是"内存不够了",果断把 -Xmx 从 4G 调到 8G 重启。结果当天夜里再次复发------因为这个 OOM 和堆内存一点关系都没有,它是操作系统拒绝为 JVM 创建新的原生线程(native thread)时抛出的错误,加堆只会让你在错误的方向上越走越远。
重启后恢复、过几天又复发,是这类问题的典型节奏:说明线程数(或系统调度实体数)在缓慢爬升,最终顶到某一层"天花板"。要根治,必须先搞清楚 Linux 到底在哪几层限制了线程数量、每一层怎么查、怎么改,以及 JVM 侧如何配合。这篇文章就把这套"五层限制"的排查方法完整梳理一遍,文末附一张速查表和决策树,可以当 SOP 用。
二、原理:JVM 的线程就是操作系统的线程
HotSpot JVM 在 Linux 上采用 1:1 线程模型:Java 里每 new Thread() 并 start() 一次,底层就是一次 pthread_create 系统调用,内核随之创建一个新的调度实体(task_struct)。也就是说:
- Java 线程 ≈ 内核眼里的"轻量级进程",线程本质上是运行在共享内存空间里的进程;
- 创建线程要闯过操作系统设置的一道道关卡,任何一道被拦下,JVM 就抛
Unable to create new native thread; - 这些关卡由粗到细分别是:系统级 PID 数 → 系统级线程总数 → 用户级进程数配额 → 进程级虚拟内存/线程栈 → VMA 映射数量,云原生环境下还要再加一层容器/cgroup 限制。
下面逐层拆解。每一层都按"是什么 → 怎么查 → 怎么改 → 什么场景会命中"来讲。
三、第一层:系统级最大进程数 kernel.pid_max
3.1 是什么
kernel.pid_max 限制的是整个系统内进程 ID 的最大值。由于线程在内核视角下也是一个调度实体,同样占用 PID 空间,所以它实际上约束了"全系统进程 + 线程的总数"。线程与进程的显著区别只在于:线程运行在共享内存空间,进程运行在独立内存空间------在 PID 这个维度上二者是等价的。
3.2 怎么查
bash
sysctl -a | grep kernel.pid_max
kernel.pid_max = 32768
# 输出表示当前系统允许的最大进程数为 32768
cat /proc/sys/kernel/pid_max
# 与上面等价
常见默认值:小内存机器是 32768;64 位大内存机器内核会自动放大,可达 4194304(约 419 万),此时这一层基本碰不到。
3.3 怎么改
bash
# 临时生效(重启失效)
echo 200000 > /proc/sys/kernel/pid_max
# 永久生效:写入 /etc/sysctl.conf 后执行
sysctl -p
3.4 什么场景会命中
机器上跑了大量任务进程(每个 JVM 动辄几百上千线程)、又叠加了细粒度的监控采集进程时,32768 的默认值有可能被吃满。命中时的特征是:全系统所有用户都创建不了新进程/线程 ,连 ssh 新开连接、执行新命令都可能失败,这是它与后面几层最直观的区别。
四、第二层:系统级最大线程数 kernel.threads-max
4.1 是什么
/proc/sys/kernel/threads-max 限制系统级最大线程(调度实体)总数,只数线程、不含传统进程。它的默认值由内核根据物理内存计算(内存越大默认越大),所以大内存服务器这一层也较少命中,但值得一并纳入巡检。
4.2 怎么查、怎么改
bash
cat /proc/sys/kernel/threads-max
# 临时修改系统级最大线程数
echo 120000 > /proc/sys/kernel/threads-max
# 永久修改同样走 /etc/sysctl.conf
注意:Linux 并没有"每个进程单独的最大线程数"这样直接的系统参数------单进程的限制是由后面第三、四、五层间接形成的。
五、第三层:用户级进程配额 ulimit -u(最高频命中的坑)
5.1 是什么
ulimit -u(max user processes)限制单个用户在同一时间能拥有的总进程数(含线程)。它是最容易命中、也最容易被误解的一层:
- 它是用户级总额度,不是单进程额度。同一账号下跑 5 个 JVM 进程,每个进程 2000 线程,消耗的就是 10000 的配额;
- 一旦某个进程线程泄漏把额度吃光,同账号下其他健康进程也会跟着创建线程失败------表现为"好几个服务同时报 OOM",非常干扰定位。反过来利用这个特征,也能快速把嫌疑锁定到这一层。
5.2 怎么查
bash
ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 515005
max locked memory (kbytes, -l) 64
open files (-n) 4096
stack size (kbytes, -s) 10240
max user processes (-u) 1024
virtual memory (kbytes, -v) unlimited
重点关注两行:max user processes (-u) 1024 是用户默认的进程(含线程)配额;stack size (-s) 10240 是默认栈大小 10M(与第四层相关)。
还有一个更可信的查法------直接看目标进程的真实限制(比登录 shell 里的 ulimit 更贴近事实,服务进程和你的 shell 环境往往不同):
bash
cat /proc/<目标进程PID>/limits | grep processes
Max processes 1024 4096 processes
5.3 怎么改
bash
# 临时修改,只影响当前 shell 及其子进程
ulimit -u 32000
永久修改要写 /etc/security/limits.conf:
bash
* soft nproc 65535
* hard nproc 65535
5.4 大坑:systemd 服务不读 limits.conf
这是云时代该问题的第一大坑:通过 systemd 拉起的服务,完全不走 /etc/security/limits.conf。那个文件是 PAM(pam_limits 模块)的配置,只对登录会话生效。你以为改了配额,服务进程的 RLIMIT_NPROC 纹丝不动,改完照样报错。
正确做法是给 systemd 服务设置 LimitNPROC:
bash
# 查看 systemd 服务的当前限制
systemctl show <服务名> | grep -i limitnproc
# 修改:编辑 unit 文件或添加 drop-in
systemctl edit <服务名>
# 写入
[Service]
LimitNPROC=65535
systemctl daemon-reload && systemctl restart <服务名>
同样地,容器化部署还有独立的 pids 限制:Docker 的 --pids-limit、Kubernetes 的 PodPidsLimit、cgroup 的 pids.max,它们与传统 ulimit 互不影响,谁小谁先生效。排查容器里的 Java 进程时,别忘了把 cgroup 这一层也查一遍。
六、第四层:进程级虚拟内存与线程栈大小
6.1 硬控制公式
Linux 对最大线程数有一条硬性控制公式:
最大线程数 ≈ 进程可用虚拟内存 / 线程栈大小
每个线程都要独占一段栈内存(默认约 1M),线程数上限由此被虚拟地址空间除以栈大小封顶。想要更多线程,只有两条路:增加虚拟内存,或减小线程栈。
6.2 相关参数
bash
ulimit -v # 进程虚拟内存上限(64 位机器通常 unlimited)
ulimit -s # 栈大小,默认常见 10240(10M)
对 32 位 JVM 这是致命一层:用户态地址空间约 3G,按每线程 1M 栈算,理论极限也就两三千线程,再怎么调系统参数都没用,只能减小 -Xss 或升级 64 位。64 位进程地址空间近乎无限,这层一般不再是瓶颈------除非有人显式设置了 ulimit -v,把 JVM 的堆、元空间、线程栈、直接内存全部关在一个笼子里,此时堆调大反而会让可用线程数变少。
6.3 减小栈的代价
减小线程栈有助于创建更多线程,但栈过小会抛 java.lang.StackOverflowError ------常见于深递归调用或方法内局部变量过大的代码(一个经典场景是旧版 Maven 编译大工程失败)。所以 -Xss 是"以安全换容量"的调优点,不是无成本的旋钮,第八节展开。
七、第五层:vm.max_map_count(VMA 数量暗坑)
7.1 是什么
vm.max_map_count 限制一个进程可以拥有的 VMA(虚拟内存区域)数量,默认约 6.5 万(65530)。每个线程栈至少要占 2 个 VMA(栈本体 + 保护页),加上堆、mmap 的文件、共享库、代码段------进程生命周期中每次映射文件、链接共享内存段、分配堆空间都会创建 VMA。
线程数上万的进程,VMA 总数很容易悄悄逼近上限。VMA 耗尽时内核无法再映射新内存,表现同样是"无法分配"类错误 ;在 JVM 里的呈现形态可能是 java.lang.OutOfMemoryError: Map failed(内存映射失败),也可能是 native thread 创建失败------因为它和线程栈映射在同一个内核路径上。
7.2 怎么查、怎么改
bash
cat /proc/sys/vm/max_map_count
# 看目标进程当前的 VMA 数量,与上限对比
cat /proc/<目标进程PID>/maps | wc -l
# 临时修改
echo 600000 > /proc/sys/vm/max_map_count
# 永久修改(RocketMQ 等组件官方就建议调到默认值的 10 倍)
echo 'vm.max_map_count=655360' >> /etc/sysctl.conf
sysctl -p
7.3 各层参数的关联
把五层串起来看就非常清晰了:
| 层级 | 参数 | 作用域 | 约束对象 |
|---|---|---|---|
| 一 | kernel.pid_max |
全系统 | 进程 + 线程总数 |
| 二 | kernel.threads-max |
全系统 | 线程总数 |
| 三 | ulimit -u / LimitNPROC / pids-limit |
单用户/单服务/单容器 | 该用户(服务、容器)的进程 + 线程总额 |
| 四 | ulimit -v、ulimit -s、-Xss |
单进程 | 虚拟内存 ÷ 栈大小 |
| 五 | vm.max_map_count |
单进程 | VMA 映射数量 |
线程本质上是共享内存空间的进程:pid_max 管的是独立内存空间的进程数,threads-max 管的是共享内存空间的线程数,ulimit -u 则是单个用户名下两类实体的总配额,且受前两者封顶------三层自上而下嵌套,取最小值生效。
八、JVM 侧配合:先诊断,再动 -Xss
系统参数只是"放宽天花板",线程数异常增长时更要回头审视 JVM 自身。
8.1 先看运行现状:系统里到底有多少线程
bash
cat /proc/loadavg
0.41 0.45 0.57 3/749 28174
第四列是被 / 分割的两个数字:第一个是当前可运行的调度实体数(通常小于等于 CPU 核数),第二个是当前存在的调度实体总数------上例说明系统里此刻并行存在 749 个进程/线程。
bash
ps -elf | wc -l # 当前进程数(含表头,实际减 1)
ps -elfT | wc -l # 当前线程总数,包含所有进程及其线程
ps -o nlwp= -p <PID> # 指定进程的线程数(轻量级进程数)
grep Threads /proc/<PID>/status # 与上一条等价
8.2 关键分叉:是"泄漏"还是"天花板太低"
用 jstack 拿线程快照做归类统计(jstack 位于 JDK 安装目录 bin 下,用于输出线程快照;拿不到时可用 jcmd <PID> Thread.print 或 kill -3 <PID> 让日志自行打印):
bash
jstack <PID> | grep -c '^"' # 线程总数
# 按线程名聚类(把编号归一化成 N),一眼看出数量异常的线程组
jstack <PID> | grep '^"' | cut -d'"' -f2 | sed -E 's/[0-9]+/N/g' | sort | uniq -c | sort -rn | head
- 如果
ps -o nlwp显示进程线程数持续上涨(比如从 800 涨到 4000+),jstack 里成百上千个同名线程(各种pool-N-thread-M、定时任务线程、业务自定义线程),那是线程泄漏------去修代码(线程池未复用、Thread 未关闭、每次请求新建线程),调大系统限制只是给泄漏续命; - 如果线程数稳定在几千且业务确实需要(比如网关、长连接服务、大数据处理任务),才是"天花板太低",按前五层逐层放宽。
这个分叉判断是整次排查中最值钱的一步,方向错了后面全是无用功。此前我在排查 YARN 集群任务全部挂掉的问题时也强调过同样的方法论:先定位根因层级,再动手改配置(Hadoop YARN 任务全部挂掉排查实录)。
8.3 调整 JVM 线程栈大小 -Xss
先确认 JVM 默认线程栈大小:
bash
java -XX:+PrintFlagsFinal -version | grep ThreadStackSize
intx ThreadStackSize = 1024 # 单位 KB,即默认 1M
通过 -Xss 减小每个线程的栈:
bash
JAVA_OPTS="-Xms4g -Xmx4g -Xss256k"
# 线程栈从 1M 降到 256k,同样的虚拟地址空间可容纳约 4 倍线程
两条原则:
- 一般不主动调小 。256k 对多数业务够用,但深递归、复杂序列化栈深的代码会直接
StackOverflowError,调之前先确认调用链深度; - 反过来,如果线程总量本就不大,也不必为了"安全"把栈调到 2M、4M------那是白白压低第四层的容量上限。
另外把视角拉远一点:线程数、打开文件句柄(ulimit -n)、max_map_count 这类"非堆资源",都应该纳入平台容量规划的输入项 ,和磁盘、内存一起做水位测算与告警(可参考我之前写的大数据平台容量规划实战)。等线上炸了再查,永远慢一步。
九、排查决策树与速查表
遇到 Unable to create new native thread,按这个顺序走:
text
1. cat /proc/<PID>/limits → Max processes 是多少?命中 ulimit -u / LimitNPROC?
2. cat /proc/loadavg → 系统调度实体总数逼近 pid_max?
3. cat /proc/sys/kernel/threads-max + pid_max → 系统层是否到顶?
4. cat /proc/<PID>/maps | wc -l → VMA 数逼近 max_map_count?(或直接报 Map failed)
5. jstack <PID> | grep -c '^"' → 线程数在涨吗?同名线程成百上千?
├─ 在涨 → 线程泄漏,修代码(治本)
└─ 稳定 → 放宽对应层限制 + 评估 -Xss(止血+扩容)
一页速查表(建议收藏):
| 限制 | 查看 | 临时修改 | 永久修改 | 常见默认 |
|---|---|---|---|---|
| 系统进程+线程数 | cat /proc/sys/kernel/pid_max |
echo 200000 > /proc/sys/kernel/pid_max |
/etc/sysctl.conf |
32768 / 大内存更高 |
| 系统线程数 | cat /proc/sys/kernel/threads-max |
echo 120000 > /proc/sys/kernel/threads-max |
/etc/sysctl.conf |
随内存计算 |
| 用户进程配额 | ulimit -u、cat /proc/<PID>/limits |
ulimit -u 32000 |
/etc/security/limits.conf(仅登录会话) |
1024 / 4096 |
| systemd 服务 | `systemctl show |
grep LimitNPROC` | --- | systemctl edit 加 LimitNPROC=65535 |
| 容器 pids | docker exec <c> cat /sys/fs/cgroup/pids/pids.max |
--- | --pids-limit / PodPidsLimit |
视运行时而定 |
| 进程 VMA 数 | cat /proc/sys/vm/max_map_count |
echo 655360 > /proc/sys/vm/max_map_count |
/etc/sysctl.conf |
65530 |
| JVM 线程栈 | `java -XX:+PrintFlagsFinal -version | grep ThreadStackSize` | 启动参数 -Xss256k |
同左 |
十、踩坑点汇总
- 见 OOM 就加 -Xmx :该错误与堆无关;32 位进程或设置了
ulimit -v的场景下,堆越大可用线程反而越少。 - limits.conf 改了没效果 :systemd 服务不读它,用
LimitNPROC;改完用cat /proc/<PID>/limits验证,不要信 shell 里的ulimit。 - 多个服务同时报错 :大概率是
ulimit -u被同账号某个进程吃光------用户级总额度是共享的,这反而帮你缩小了范围。 - 只看进程数不看线程数 :
ps -elf数出来 200 很健康,ps -elfT一看 3 万------线程才是计数主体。 - 报
Map failed却去查内存容量 :先cat /proc/<PID>/maps | wc -l对比max_map_count,十有八九是 VMA 撞墙。 - 调小 -Xss 上线不压测:StackOverflowError 往往只在特定深调用链触发,灰度验证再全量。
- 治标不治本:调大限制只是提高天花板,线程泄漏不修,天花板再高也会再撞一次。
十一、结语
Unable to create new native thread 本质上不是一个 Java 问题,而是操作系统在替你回答"还允不允许你再多一个线程"。把 Linux 的五层限制(pid_max → threads-max → ulimit -u/LimitNPROC → 虚拟内存/栈 → max_map_count)记在心里,配上一条"先 jstack 分叉判断泄漏还是天花板"的主线,这类故障基本可以在半小时内定位到层。
你们在生产上撞到的是哪一层?是 systemd 的 LimitNPROC 暗坑、容器 pids-limit,还是真正的线程泄漏?欢迎在评论区聊聊你的排查过程。
相关阅读: