试分析 系统支持创建的最大线程数量 以及Java程序支持和的最大JavaThread数量

2 阅读6分钟

这是一个非常经典且极具实战意义的系统底层与 JVM 调优问题。

要分析“最大线程数量”,我们需要明确一个核心前提:在 JDK 21 虚拟线程普及之前,Java 中的普通线程(Platform Thread)与操作系统的原生线程(OS Thread)是 1:1 映射的。

因此,Java 能创建的最大线程数,不仅受限于 JVM 自身的内存划分,更受限于操作系统的资源上限与内核参数。我们分“操作系统(以 Linux 为例)”和“Java 程序”两个层面来深度剖析。


第一部分:操作系统(Linux)支持的最大线程数量

操作系统层面限制最大线程数的因素主要分为两类:物理内存(硬限制)内核配置(软限制)

1. 物理内存与栈大小(硬限制)

在 Linux 中,每个线程都需要拥有自己独立的线程栈(Stack)。

ulimitLinux/Unix 系统下用于 查看或设置 shell 各类资源限制 的命令。
-s 选项专门用来设置或查看 栈(stack)大小限制

  • 栈 (stack) :程序运行时用于存储函数调用的局部变量、返回地址等的数据结构。
  • 栈大小限制过小,可能会导致 递归调用过深时出现 “Segmentation fault” 错误。

Linux 默认的线程栈大小通常是 8MB(可以通过 ulimit -s 查看)。

image.png

  • 理论公式理论最大线程数 = 空闲物理内存 / 线程栈大小
  • 举例:如果你有一台 8GB 内存的纯净服务器,抛开系统运行所需的 1GB,剩余 7GB。7000MB / 8MB ≈ 875 个线程(注意:这只是粗略计算,现代 OS 有虚拟内存和写时复制机制,实际数字会稍大,但内存绝对是第一物理瓶颈)。

2. Linux 内核参数(软限制)

为了防止耗尽系统资源导致宕机,Linux 做了多层内核级拦截:

2.1 kernel.pid_max

kernel.pid_max 是 Linux 内核中的一个参数,用于 限制系统中进程 ID(PID)的最大值。

PID(Process ID) 是每个进程在系统中的唯一标识。

核心作用: 防止 PID 数值过大导致系统无法管理。 影响系统中最多可以同时存在的进程数量(理论上)。

  • kernel.pid_max:在 Linux 底层,线程被视为轻量级进程(LWP),每个线程都需要一个唯一的 PID。
    • 默认值通常是 32768。这意味着系统最多只能有 3 万多个线程+进程。可以修改 sysctl -w kernel.pid_max=4194304 来放宽。

image.png

2.2 kernel.threads-max

kernel.threads-max:内核允许创建的系统全局最大线程数。

  • kernel.threads-max 是 Linux 内核中的一个参数,用于 限制系统中最多可以同时存在的线程数量

  • 注意:

    • 在 Linux 中,线程和进程在内核层面几乎是一样的(都是 task_struct),区别主要在于 线程共享内存空间
    • 所以 threads-max 也可以理解为 系统中最多可同时存在的任务数量(线程 + 进程)。

作用

  • 控制系统资源:

    • 每个线程都需要占用一定的内存(task_struct、内核栈)。
    • 如果线程数无限制增长,可能耗尽系统内存,导致系统不稳定。
  • 防止恶意或 buggy 程序创建大量线程,保护系统安全。

image.png

2.3 vm.max_map_count
  • vm.max_map_count (极易踩坑):限制一个进程可以拥有的最大内存映射区域数。每个线程的栈都需要一个独立的 VMA(虚拟内存区域)。
    • 默认值通常是 65530。当 Java 程序尝试创建超过 6 万个线程时,即使内存充足,也会被系统无情拒绝。

image.png

image.png

2.4 ulimit -u
  • 用户级限制 (ulimit -u / nproc):限制特定用户可以创建的最大进程/线程数。

image.png

image.png

第二部分:Java 程序支持的最大 JavaThread 数量

对于 Java 程序而言,由于 1:1 的映射关系,它首先受到上述所有 Linux 限制的约束。除此之外,JVM 自身的内存模型决定了它的特殊性。

1. 决定性因素:非堆内存(Native Memory)

这是一个非常违反直觉的陷阱:Java 线程的栈内存,是不分配在 JVM 堆(Heap)里面的,而是分配在操作系统的本地内存(Native Memory)中!

这就得出了一个极其重要的公式: 可用本地内存 = 操作系统总内存 - JVM 堆内存(-Xmx) - 元空间(Metaspace) - JVM 内部开销 - 操作系统保留 Java 最大线程数 = 可用本地内存 / Java 线程栈大小(-Xss)

【经典反直觉案例】: 假设你有一台 8GB 内存的服务器,为了“优化”性能,你把 JVM 堆内存 -Xmx 设置为 7.5GB结果:只剩下 0.5GB 的内存留给操作系统和非堆内存。此时你最多只能创建两三百个 Java 线程,系统就会抛出可怕的: java.lang.OutOfMemoryError: unable to create new native thread 结论:分配给 JVM 堆(Heap)的内存越大,能留给创建 Java 线程的内存就越少!

2. JVM 参数设置

  • -Xss (Thread Stack Size):决定每个 Java 线程占用的栈大小。
    • 64 位 JVM 默认通常是 1MB
    • 如果你想在有限的内存里创建更多的 Java 线程,可以调小它(比如 -Xss256k)。但代价是容易引发 java.lang.StackOverflowError(尤其是在递归调用或深度框架调用,如 Spring AOP 层级很深时)。

第三部分:综合推演与性能真相

假设我们有一台 16GB 内存 的主流服务器,并且我们解除了 Linux 的所有内核参数限制(pid_max, max_map_count 等拉满)。

  • 场景设置:JVM 堆内存设为 8GB(-Xmx8G),操作系统及其他保留 2GB。剩余给 Native Memory 的大约是 6GB
  • 栈大小:采用默认的 -Xss1m
  • 理论极限计算6GB (6144MB) / 1MB = 6144 个线程

所以,在一个标准的互联网生产环境中,一个 Java 进程的极限线程数量通常在 3000 到 10000 之间。如果通过极限压缩 -Xss128k,并且把 Heap 压到极小,理论上可以创建几万个线程。

⚠️ 性能的残酷真相(C10K 问题)

仅仅讨论“能创建多少”是没有意义的,因为操作系统线程是非常昂贵的计算资源

  • 当你创建了超过 2000 个活跃的 Java 物理线程时,操作系统会将绝大部分 CPU 算力消耗在 线程上下文切换(Context Switch) 上(保存寄存器、切换内核态、刷新 TLB 缓存等)。
  • 业务还没开始跑,CPU 已经被系统调度榨干了。表现为 CPU 使用率 100%,但系统的吞吐量(TPS)断崖式下跌。

总结与新时代的降维打击 (Project Loom)

  • 系统/OS 极限:受限于内存总量和内核限制(通常几万个)。
  • Java 物理线程极限:受限于 Native Memory 大小和 -Xss,并且与 -Xmx 成反比(通常几千到一两万个)。抛出的异常是 unable to create new native thread

这就是为什么要有“虚拟线程(Virtual Threads)”! 因为传统的物理 Java Thread 已经被操作系统的天花板死死卡住了(撑死几千个)。而虚拟线程把栈存在了 JVM 的堆(Heap)里,随着需要动态扩容(初始只有几百字节)。在同样的 16GB 机器上,物理线程只能开几千个,而虚拟线程可以轻松开启 500万 到 1000万个,且完全没有 OS 上下文切换的开销,彻底颠覆了并发编程的极限。