八成是用户级线程数限制(ulimit -u)导致,而非JVM或内核参数;需用prlimit -n $PID验证Java进程实际nproc限制,因systemd、Docker、K8s等不同启动方式加载机制各异,/etc/security/limits.conf在非PAM登录场景下常不生效。

Java 进程报 java.lang.OutOfMemoryError: unable to create new native thread,但堆内存充足?八成是卡在用户级线程数限制上,而不是 JVM 或内核参数。
查当前 shell 的 ulimit -u 值
这个值直接决定 Java 进程能创建多少线程(因为 Linux 线程即轻量进程,ulimit -u 限制的是“用户所有进程 + 线程总数”):
- 运行
ulimit -u—— 普通用户默认常为1024或4096,远低于高并发 Java 应用所需 - 对比
ps -eLf | wc -l输出:若两者接近,基本可断定是它在卡你 - 注意:该限制继承自启动 Java 的 shell;如果 Java 是 systemd 服务启的,
ulimit -u在终端里查不到真实值,得看 service 配置
查 Java 进程实际生效的 nproc 限制
Java 启动后,它真正受哪一层 nproc 约束,得看它启动时继承的上下文:
- 普通命令行启动:
prlimit -n $PID查MAX NPROC字段(即ulimit -u的硬限制) - systemd 服务启动:必须检查
systemctl show myapp.service | grep LimitNPROC,默认不读/etc/security/limits.conf - Docker 容器内:
docker inspect $CONTAINER_ID | grep -i ulimit,确认是否传了--ulimit nproc=65535:65535 - Kubernetes Pod:检查
securityContext.limits.nproc是否设置,否则走节点默认值(常为1024)
为什么只改 /etc/security/limits.d/90-nproc.conf 不一定生效
改完配置文件,很多人发现 Java 还是只能开 1024 个线程——不是配置没写对,而是没触发加载时机:
-
limits.conf和limits.d/*.conf只在 PAM 登录会话中加载(如 ssh、console login),非登录 shell(比如 cron、systemd、容器 init)完全不走这套流程 - systemd 默认禁用 PAM limits;要全局生效,得在
/etc/systemd/system.conf加DefaultLimitNPROC=65535,再systemctl daemon-reload - Java 进程若由另一个守护进程 fork 出来(如 supervisord、jenkins agent),它继承的是该父进程的限制,不是你当前终端的
- 验证是否生效:重启服务后,立刻执行
prlimit -n $PID,别信配置文件写了就等于生效
别被 top 或 htop 的 “Tasks” 数骗了
这两个工具显示的线程数严重偏低,不能用于判断是否触顶:
-
top默认只显示屏幕高度内的行数(比如 50 行),根本刷不完全部线程 -
htop默认过滤掉内核线程(如[kthreadd]、[rcu_gp]),而它们也占用threads-max和ulimit -u配额 - 唯一可靠统计当前总线程数的命令是:
ps -eLf | tail -n +2 | wc -l—— 它不依赖终端尺寸,不做过滤,字段稳定,普通用户可执行
真正麻烦的不是改哪个数字,而是弄清 Java 进程到底跑在哪一层调度上下文中:shell?systemd?容器?K8s?不同环境加载限制的机制完全不同,同一份 limits.conf 在这台机器生效,在另一台可能完全被忽略。


















