Java线程创建失败:Unable to Create New Native Thread的排查与修复全指南
干 Java 的兄弟应该都见过这个报错:unable to create new native thread。它通常挂着一串 atjava.lang.Thread.start的调用栈冒出来,而且每次都挑在最忙的时候——线上业务正压着峰值,突然一批任务全部失败,日志里整整齐齐刷出这个异常。我先说结论:这不是"JVM 坏了",而是 JVM 尝试向操作系统申请新的线程资源时被系统拒绝了。至于为什么拒绝,可能是线程栈内存不够、进程线程数触顶、系统线程总量超限,或者容器限制卡住了你。这篇文章把我实际排查和修复这个问题的完整思路、命令、参数以及藏得比较深的坑全部写出来,不讲空话,照着做就行。
1. 先搞懂一件事:这个报错本质上是操作系统在拒绝你
1.1 Java 每个线程到底是怎么来的
很多写 Java 的人会把线程和 JVM 内部概念搞混,觉得 new Thread() 就是在 JVM 里"画"一个对象出来。实际上,HotSpot 虚拟机里每个 Java 线程都对应一个原生线程(native thread),调用Thread.start()之后,JVM 会通过pthread_create这类系统调用,让操作系统去真正创建一个内核线程。
这个过程需要向操作系统申请两样东西:线程控制块(内核资源)和一块栈内存。64 位 Linux 上 HotSpot 默认给每个线程的栈大小是 1MB,加上 JVM 自身要维护线程相关的元数据,实际上每个线程在进程虚拟内存里的占位会略大于 1MB。
所以问题来了:操作系统不是无限资源的提供方,它给每个用户、每个进程、整个系统都设了各种上限。一旦任何一层限制被顶到,pthread_create就会失败,JVM 拿到返回的错误码,就抛出了那个著名的java.lang.OutOfMemoryError: unable to create new native thread。
1.2 哪些场景最容易触发这个错误
根据我这几年的排查经验,遇到这个报错大致逃不出下面这几类情况:
第一类是线程池被占满后还在大量创建新线程。比如某支付网关项目用的框架默认线程池只有 200 个线程,但是下游服务响应特别慢,导致任务全部堆积在线程池队列里,请求还在不断进来,线程池按配置持续创建新线程,很快就撞上系统限制。
第二类是代码里到处 new Thread。后台任务调度平台最典型,定时任务里每来一批任务就 new 一批线程,任务处理不完又开新任务,线程只增不减,最终进程直接宕掉。
第三类是部署环境本身就限制了线程数量。有些基础镜像会把 nproc(用户最大进程数)设得很低,JVM 还没跑满业务量就先触顶了。
第四类是容器内存配置不合理。这种情况最隐蔽:容器设置了内存上限,但 JVM 启动时识别到的可用内存比实际能用的多,导致线程栈内存占用远超预算,内存先耗光再报无法创建线程。
这里我统一强调一下:遇到这个错,优先当"操作系统资源问题"处理,别一头扎进业务代码里去翻。
2. 排查这种问题,我建议你按这个顺序来
2.1 先确认线程数到底是不是真的失控了
拿到报错之后第一件事,不是改参数,而是搞清楚 JVM 当前到底创建了多少线程。如果连数据都没有,改完参数也是瞎蒙。
最快速的办法是直接数进程里的线程:
# 先找到 Java 进程 PID ps -ef | grep java # 统计该进程下线程数量 ps -eLf | grep <PID> | wc -l # 或者用 jcmd 直接打印线程信息 jcmd <PID> Thread.print | grep -c "^\""我自己常用的是jcmd Thread.print,因为它能顺带看到线程名和栈状态。如果发现线程数已经上千,并且日志里大量任务被阻塞,那基本可以肯定线程已经失控了。这时候别急着杀进程,先把线程转储留下来:
jstack <PID> > /tmp/jstack_$(date +%s).log这个文件后面排查线程大头时非常有用。你可能还会看到很多RUNNABLE状态的线程卡在同一个 socket read 上,那基本就是下游依赖响应慢把线程拖死了。
2.2 系统层的三重限制逐一核对
线程数失控之后,要顺着"操作系统为什么拒绝创建线程"这条路往下查。这里有三层限制,缺一不可。
第一层是用户级限制。Linux 对单个用户可以创建的进程/线程总数有限制,用ulimit -u查看:
ulimit -u需要注意,这个值在 Linux 下同时限制进程数和线程数。默认值通常是 1024 或 65535,但很多环境会被调小。
第二层是系统全局限制。整个 Linux 系统能创建的线程总数受threads-max限制:
cat /proc/sys/kernel/threads-max第三层是进程 PID 数量上限,也就是pid_max:
cat /proc/sys/kernel/pid_max还有一个容易被忽略的:进程虚拟内存映射区数量上限max_map_count。每个线程栈在进程里都是一个内存映射区域,创建大量线程时,映射区数量也可能触顶:
cat /proc/sys/vm/max_map_count三层限制的关系可以用一个比较土的类比:线程就是新住户,ulimit -u是这个小区的总户数限制,threads-max是整个城市的户口总量,max_map_count是小区里能划的地块数。任何一个满了都住不进去。
2.3 JVM 自身的内存参数也要盯
线程创建失败还有一个非常常见的原因是进程的虚拟内存不够了。每个线程栈默认 1MB,这个值在 64 位 Linux 上看起来不大,但线程数量上去之后非常可观。1000 个线程就是 1GB 虚拟内存,5000 个线程就是 5GB。
如果你启动 JVM 时设置了-Xmx很大,同时系统虚拟内存本身不够,操作系统就会在创建线程时直接拒绝内存分配。
另外还有一个冷门但很关键的点:操作系统内存 overcommit 策略。运行cat /proc/sys/vm/overcommit_memory,如果值是 2,内核会按"承诺不能超过总量"来严格限制虚拟内存分配。这种情况下即使物理内存充足,虚拟内存超额也可能导致线程创建失败。
我自己踩过一次:某后台系统-Xmx设了 8G,-Xss没动,默认 1MB,高峰线程数到了 6000 左右,再加上堆内存、元空间、直接内存,进程虚拟内存直接冲到了 15G 以上,而机器的 overcommit 策略是 2。结果就是每次线程数冲到 6000 多一点就报这个错,不早不晚刚刚好。
3. 修复方案就三板斧:系统层、JVM层、代码层
3.1 操作系统层参数调整
如果确认是用户级限制导致的,可以用两种方式调整。临时生效:
ulimit -u 65535持久生效,修改/etc/security/limits.conf,加入:
* soft nproc 65535 * hard nproc 65535注意,nproc限制的是"用户进程数",包含线程。修改完需要重新登录会话或者重启进程才生效。
如果是系统级threads-max不够,可以用 sysctl 调整:
sysctl -w kernel.threads-max=200000持久生效就写进/etc/sysctl.conf:
kernel.threads-max=200000max_map_count同理:
sysctl -w vm.max_map_count=262144这里有个重要的坑:盲目调大系统参数只是拖延问题,不是解决根因。如果代码层无限创建线程,调再大也会再次触顶,而且线程数量过大时 CPU 上下文切换开销会拖垮整个服务。所以这三板斧要连起来用,不能只用一把。
3.2 JVM 参数的调整逻辑
JVM 层面最常见的手段是调小线程栈大小,也就是-Xss参数。
java -Xss256k -jar app.jar把默认 1MB 改成 256k 或 512k,能让同样内存下容纳更多线程。但注意:栈变小,递归深度会受限,如果代码里有较深的递归调用,可能出现StackOverflowError。所以在调整-Xss之前,先确认系统里有没有递归或者超大局部变量的代码路径。
另外,JDK 1.8 之后还可以用-XX:ThreadStackSize来设置,效果和-Xss等同。不过一般写-Xss就够了。
还需要注意-Xss只影响原生线程栈的初始大小,不代表线程内存占用就是精确的 256k。实际每个线程还会有 JVM 内部结构,所以只能说"大约降低了不少"。
如果是容器环境,还需要检查-Xmx设置是否合理。常用做法是配合-XX:MaxRAMPercentage:
java -XX:MaxRAMPercentage=75.0 -Xss256k -jar app.jar这个参数告诉 JVM,在容器限制的物理内存里,最多拿 75% 给堆。这样能避免 JVM 以为内存很多、实际一跑就爆的情况。
3.3 代码层治理才是治本手段
系统参数和 JVM 参数都是"拓宽道路",代码层要做的才是"减少车流量"。
先说最常见的病根:直接 new Thread。这种代码一旦出现在任务处理逻辑里,基本是定时炸弹。正确做法是全部收敛到线程池:
ExecutorService executor = new ThreadPoolExecutor( 10, // 核心线程数 200, // 最大线程数 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000), new ThreadFactoryBuilder().setNameFormat("task-%d").build(), new ThreadPoolExecutor.CallerRunsPolicy() );这里的参数不是随便拍的。核心线程数和最大线程数要根据任务类型来定:CPU 密集型任务线程数大约等于 CPU 核数 + 1,IO 密集型可以根据"核数 * (1 + 等待时间/计算时间)"估算。队列长度需要根据任务堆积时长上限来倒推。
还有一个不可省的设置:拒绝策略。默认的AbortPolicy会在队列满时直接抛异常,导致任务丢失。实际操作里我更推荐CallerRunsPolicy,它让提交任务的线程自己执行任务,起到天然限流的作用,至少不会让任务静默消失。
另外,如果系统里有多组业务在共用线程资源,应该做线程池隔离,别让一个慢服务拖垮所有业务线程。这一点在微服务架构里尤其重要:一个下游超时,不能把上游的线程池全部占完。
4. 用一个小 Demo 完整复现从报错到恢复的全过程
4.1 构造一个可控的复现环境
纸上谈兵没意思,我用一个简单 Demo 来模拟线上场景。假设有一个后台任务系统,任务处理用了无界线程池:
public class ThreadLeakDemo { public static void main(String[] args) { while (true) { new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(Long.MAX_VALUE); } catch (InterruptedException e) { // ignore } } }).start(); } } }这个代码会无限创建线程,每个线程都 sleep,不释放。在 Linux 下跑一小会儿就会把系统限制顶穿。
为了能安全复现,先把用户限制调小一点:
ulimit -u 512然后启动这个 Demo:
java -Xmx256m ThreadLeakDemo很快就能看到:
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread at java.lang.Thread.start0(Native Method) at java.lang.Thread.start(Thread.java:717)4.2 一步步处理完整个流程
第一步先看进程状态:
ps -eLf | grep ThreadLeakDemo | wc -l你会发现线程数卡在 500 多就上不去了。同时查看系统限制:
ulimit -u cat /proc/sys/kernel/threads-max到这里问题已经很清晰:ulimit -u 512卡住了线程数。接下来我演示三种修法:
修法一:调大用户限制
ulimit -u 4096重新启动 Demo,线程数能继续往上走。但这不是治本,因为内存总会被耗完。
修法二:修改 JVM 栈大小
java -Xmx256m -Xss128k ThreadLeakDemo同样的限制下,线程创建数量明显变多了。因为每个线程占用的虚拟内存变小了,但这样做也只是把触顶的位置往后推。
修法三:改造代码
把无限new Thread改成有界线程池:
ExecutorService executor = new ThreadPoolExecutor( 10, 10, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(100) ); while (true) { executor.submit(() -> { try { Thread.sleep(Long.MAX_VALUE); } catch (InterruptedException e) { // ignore } }); }这种写法下线程数最多 10 个,队列堆满后会触发拒绝策略,不会再无脑创建线程。这才是在生产环境里真正需要的姿态。
跑完这个 Demo 之后,记得把ulimit -u恢复原值,别留一个受限的 shell 环境给自己。
5. 容器环境才是最考验人的地方
5.1 JVM 认不准容器内存限制
现在很多服务跑在容器里,问题往往不在宿主机,而在容器这一层。
JDK 8 在早期版本里并不能自动识别 cgroup 的内存限制。如果你用-Xmx没显式设置,JVM 会拿宿主机的物理内存来算默认值。结果就是 JVM 以为有 64G 内存可用,实际上容器只给了 4G 配额,堆内存 + 线程栈 + 元空间一叠加,容器内存直接被打满,然后线程创建失败。
这个问题在 JDK 8u131 之后有改善,引入了-XX:+UseCgroupMemoryLimitForHeap,到 8u191 之后默认开启了容器识别。但如果你还在用比较老的 JDK 8 版本,或者用的是自定义基础镜像,建议显式设置:
java -XX:MaxRAMPercentage=75.0 -Xss256k -jar app.jarMaxRAMPercentage会按容器限制的物理内存来计算堆大小,比手写-Xmx更稳妥,至少不会因为机器内存大小不同而出现配置漂移。
5.2 cgroup 的 pids 限制是另一个隐形天花板
内存之外,容器还有线程数限制。现代容器运行时会往/sys/fs/cgroup/pids/pids.max写一个值,这个值限定了容器内所有进程加起来的线程总数。
查看当前容器的限制:
cat /sys/fs/cgroup/pids/pids.max # 查看已经用了多少 cat /sys/fs/cgroup/pids/pids.current如果pids.current已经非常接近pids.max,并且系统日志里出现找不到 PID 资源之类的错误,那就基本能确定了。
这个限制在 Kubernetes 环境里通常是由 Pod 的资源申请间接映射过来的,一般不会直接暴露给应用。但一旦线程数触顶,Java 进程照样报unable to create new native thread。
处理方式是在启动参数层面保证线程数可控,比如调小-Xss、限制线程池最大线程数。同时给 Pod 配一个合理的进程/线程配额,别让所有服务共享一个过于紧张的上限。
5.3 ulimit 改完不生效的原因
容器里还有一个特别搞心态的情况:你在容器里执行ulimit -u怎么改都改不动,或者改了立即失效。
这是因为容器进程的RLIMIT_NPROC是在启动时由容器运行时设置的,shell 里再改通常会被限制,甚至没有权限。正确做法是在启动容器时通过参数设置,比如:
docker run --ulimit nofile=65535:65535 --ulimit nproc=65535:65535 ...在 Kubernetes 中则要调整 Pod 级别的配置,或者依赖治理平台把对应参数注入到启动命令里。我更推荐的方式还是代码层控制线程数 + JVM 参数控制栈内存,不要依赖在容器里改系统参数,因为这种改动不可靠、难追踪。
6. 别等出了事再救火:线程数的日常治理
6.1 线程池参数设计要算过,不能靠感觉
线程池的核心线程数、最大值、队列长度,这三个值必须根据业务量级算一遍,而不是网上抄一个"最佳实践"。
一个比较靠谱的估算思路:先分析任务的类型。
- CPU 密集型任务,按 CPU 核数来定,通常核数 + 1。
- IO 密集型任务,可以按核数乘以(1 + 等待时间/计算时间)估算。比如发起一次远程调用耗时 200ms,CPU 计算只占 10ms,等待比大约是 20,那么双核机器上核心线程数取 42 以上理论上是合理的,但还要留足队列空间,别让线程数直接打到最大值,否则内存压力照样大。
实际操作中,我会把核心线程数控制在常规流量水位以下,最大线程数留 10%~20% 的缓冲,队列长度按照"允许任务排队多少秒"来反推。
配置文件里也可以用占位符管理:
task: pool: core-size: 16 max-size: 64 queue-capacity: 512 thread-name-prefix: async-task- keep-alive-seconds: 60然后通过配置中心去动态调整,方便大促前临时扩大容量。
6.2 监控系统要有线程数的影子
线程数这种指标,平时没人看,出事了才觉得重要。但真等报错才去看就晚了。早期预警完全可以通过监控实现。
JVM 自身就提供了线程数的获取接口:
ThreadMXBean bean = ManagementFactory.getThreadMXBean(); int threadCount = bean.getThreadCount(); long peakThreadCount = bean.getPeakThreadCount();把这些数据暴露给监控系统,比如定时上报到指标平台,配上阈值告警:
- 线程总数超过某个基线持续 5 分钟,提醒。
- 线程数增长曲线斜率过高,提醒。
- 活跃线程数长期接近最大线程数,提醒。
这些告警能在unable to create new native thread出现之前就把问题暴露出来。我在维护某后台任务平台时,就是靠这条曲线在凌晨 3 点抓住了线程泄漏的苗头,比等到早上业务投诉再排查舒服太多了。
6.3 定期抓线程转储,别等现场丢了才后悔
线程泄漏排查最怕的就是现场没了。进程重启之后什么都看不到,只能靠重新复现。所以建议定时、主动地抓线程转储。
简单的方式是写个定时任务:
jcmd <PID> Thread.print > /data/logs/jstack_$(date +%Y%m%d_%H%M%S).log留存最近 7 天的转储文件。出问题时可以直接对比同一个业务线程在不同时间点上的数量变化,定位到是哪个调用链在无限创建线程。
在 Java 9 之后,也可以直接用jcmd Thread.dump_to_file生成.jfr格式的转储,配合分析工具看线程的锁等待关系,比纯文本直观一些。不过日常快速排查,jstack已经够用了。
7. 常见问题与快速处理速查表
这里把我这几年遇到过的场景整理成一张表,方便直接对照:
| 症状 | 常见原因 | 快速排查命令 | 处理手段 |
|---|---|---|---|
| 线程数到某个值就报错 | ulimit -u 限制 | ulimit -u | 调大 nproc 限制 |
| 线程数持续增长最终爆掉 | 代码无限创建线程 | jstack看线程名和栈 | 改线程池,设置拒绝策略 |
| 设置大 -Xss 后内存暴涨 | 线程栈内存占用过高 | ps -eLf对比线程数 | 调小 -Xss,控制线程数量 |
| 容器内线程数受限 | cgroup pids.max 限制 | cat /sys/fs/cgroup/pids/pids.max | 调整 Pod 配额或减少线程 |
| JVM 内存超预期占用 | 老 JDK 不识别容器内存 | java -XX:+PrintFlagsFinal -version查看 MaxHeapSize | 显式设 MaxRAMPercentage |
| 报错伴随内存溢出 | 堆内存 + 线程栈超预算 | jstat -gcutil <PID>查看 GC 趋势 | 降低 -Xmx,配合 -Xss 调整 |
| 修改 ulimit 不生效 | 容器内无法修改系统限制 | 直接看容器运行时参数 | 从容器启动配置调整 |
这张表不是标准答案,但是它覆盖了绝大多数线上场景。遇到报错时,逐行对照做一遍,基本能定位出来。
8. 一点个人实操体会
这个错误我前前后后处理过不下十次,印象最深的不是技术层面的复杂,而是很多人习惯性地把它当成"JVM 内存问题",一上来就调-Xmx,结果越调越糟。
我自己现在排查这类问题的固定套路是:先数线程,再看限制,再查代码,最后才动参数。方向对了,问题往往比想象中简单。
还有一个小技巧:修复之后别急着收工,把 JVM 的线程数峰值记下来,和系统限制对比一下,留出安全余量。比如系统允许 5000 个线程,业务峰值只用到 2000,那这个配置基本是稳的。如果峰值占比超过 80%,就得考虑优化代码或者扩容了。
如果这篇文章能让你少加一次班,那就值了。