Java 进程 Unable to create new native thread 排查实战:Linux 线程数五层限制与 JVM -Xss 调优

一、现象:一个"看起来像内存不足"的假象

某个周一早上,大数据平台的一个 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 倍线程

两条原则:

  1. 一般不主动调小 。256k 对多数业务够用,但深递归、复杂序列化栈深的代码会直接 StackOverflowError,调之前先确认调用链深度;
  2. 反过来,如果线程总量本就不大,也不必为了"安全"把栈调到 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 同左

十、踩坑点汇总

  1. 见 OOM 就加 -Xmx :该错误与堆无关;32 位进程或设置了 ulimit -v 的场景下,堆越大可用线程反而越少。
  2. limits.conf 改了没效果 :systemd 服务不读它,用 LimitNPROC;改完用 cat /proc/<PID>/limits 验证,不要信 shell 里的 ulimit。
  3. 多个服务同时报错 :大概率是 ulimit -u 被同账号某个进程吃光------用户级总额度是共享的,这反而帮你缩小了范围。
  4. 只看进程数不看线程数 :ps -elf 数出来 200 很健康,ps -elfT 一看 3 万------线程才是计数主体。
  5. 报 Map failed 却去查内存容量 :先 cat /proc/<PID>/maps | wc -l 对比 max_map_count,十有八九是 VMA 撞墙。
  6. 调小 -Xss 上线不压测:StackOverflowError 往往只在特定深调用链触发,灰度验证再全量。
  7. 治标不治本:调大限制只是提高天花板,线程泄漏不修,天花板再高也会再撞一次。

十一、结语

Unable to create new native thread 本质上不是一个 Java 问题,而是操作系统在替你回答"还允不允许你再多一个线程"。把 Linux 的五层限制(pid_max → threads-max → ulimit -u/LimitNPROC → 虚拟内存/栈 → max_map_count)记在心里,配上一条"先 jstack 分叉判断泄漏还是天花板"的主线,这类故障基本可以在半小时内定位到层。

你们在生产上撞到的是哪一层?是 systemd 的 LimitNPROC 暗坑、容器 pids-limit,还是真正的线程泄漏?欢迎在评论区聊聊你的排查过程。

相关阅读:

云原生日志采集与检索架构实战(K8s + 传统日志统一接入)

Flink 实时交易监控实战:分钟级指标计算

相关推荐
CS_Zero1 小时前
解决模型开发板ping不通模型终端设备问题
linux·网络
wuminyu1 小时前
Continuation::freeze与thaw的x86_64汇编Stack Chunk拆解源码分析
java·linux·c语言·汇编·jvm·c++
程序员夏洛1 小时前
TCP 是用来解决什么问题?
java·网络协议·http·https
y = xⁿ1 小时前
拆解cloud code记忆系统
java·人工智能·python·github
修船大队长1 小时前
启动阶段 KL15 未接通时触发报文超时监控
运维·服务器·网络
程序员-Benothing2 小时前
Linux网络排错命令:ping、traceroute、mtr、dig、nslookup
linux·运维·网络
轮子大叔2 小时前
Hibernate6.x详解
java
ClickHouseDB2 小时前
ClickHouse .NET 驱动程序新特性:从 1.0 到 1.3 的演进之路
java·前端·windows