一句話先說結論:這個報錯和 Java 堆沒關係。它說的是 JVM 找操作系統申請原生執行緒失敗,原因要麼是執行緒數頂到 OS 上限,要麼是執行緒棧把 native 記憶體吃空了。先查
ulimit -u和執行緒數,再決定調大限制還是減執行緒。
背景
Java 的每個執行緒,背後都對應一個操作系統原生執行緒。原生執行緒的開銷不進 Java 堆,走的是 native 記憶體。
執行緒棧是 native 記憶體的大頭。64 位 JVM 預設一個執行緒棧約 1 MB。堆可能還有一半空著,執行緒數一多,native 記憶體先扛不住。
所以這個報錯經常出現在監控顯示「記憶體還很充足」的時候,容易讓人一頭霧水。
現象
執行緒池或壓測腳本不停開執行緒,跑到某一刻報錯:
java.lang.OutOfMemoryError: unable to create new native thread
at java.base/java.lang.Thread.start0(Native Method)
at java.base/java.lang.Thread.start(Thread.java:809)
at java.base/java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:931)
at java.base/java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)
at com.example.task.AsyncTaskRunner.submit(AsyncTaskRunner.java:26)
at com.example.service.OrderService.placeOrder(OrderService.java:44)
at com.example.controller.OrderController.create(OrderController.java:31)
堆疊落在 Thread.start0,這是開原生執行緒的入口。注意它和 OutOfMemoryError: Java heap space 是兩碼事,後者才是堆滿。
根因分析
兩類原因,按頻率排。
1. 執行緒數撞上操作系統的上限
Linux 把進程和執行緒當同一種東西管。下面的限制直接影響能開多少執行緒:
ulimit -u:每個用戶能開的進程/執行緒數,常見預設是 1024。kernel.threads-max:系統級執行緒總數上限。kernel.pid_max:最大 PID,也間接卡住執行緒數。- 容器裡的 cgroup
pids限制。 - systemd 管理的服務,另有一個
UserTasksMax。
2. native 記憶體被執行緒棧耗盡
每個執行緒棧預設約 1 MB。開 1 萬個執行緒,光棧就要約 10 GB native 記憶體。執行緒多,或者 -Xss 設太大,都會把 native 記憶體耗光。
排查步驟
先確認是哪種原因。
看用戶執行緒上限:
ulimit -u
看系統級參數:
cat /proc/sys/kernel/threads-max
cat /proc/sys/kernel/pid_max
cat /proc/sys/vm/max_map_count
數一下當前進程開了多少執行緒:
jstack <pid> | grep "java.lang.Thread.State" | wc -l
執行緒數貼近限制,基本就定位了。
解決方案
按根因對症下藥。
調高用戶執行緒上限
臨時放開,讓這個 shell 能開更多執行緒:
ulimit -u 8192
永久改,寫 /etc/security/limits.conf:
foo hard nproc 50000
foo soft nproc 50000
foo hard nofile 60000
foo soft nofile 60000
調內核參數
系統級上限不夠時:
sysctl -w kernel.threads-max=100000
sysctl -w kernel.pid_max=400000
sysctl -w vm.max_map_count=500000
寫進 /etc/sysctl.conf 才能重啟後生效。
systemd 服務改 UserTasksMax
用 systemd 拉起的服務,檢查 /etc/systemd/system.conf:
UserTasksMax=60000
改完執行 systemctl daemon-reload。
從根上減執行緒
調限制是治標,治本是少開執行緒。
- 把無界的
newCachedThreadPool換成有界執行緒池。 - 控制壓測和併發度。
- 真有大量併發,Java 21 的虛擬執行緒更合適,它不佔原生執行緒。
減小執行緒棧
確需大量原生執行緒時,可以壓縮單個棧:
java -Xss512k ...
預設 1 MB,降到 512 KB 能讓同等 native 記憶體容納更多執行緒。但棧太小有 StackOverflowError 風險,得實測。
思考過程
排查時最容易犯的錯,是看到 OutOfMemoryError 就調大 -Xmx。這個錯誤不歸 -Xmx 管,調了沒用。
先分清「堆滿」和「執行緒滿」是兩類問題。堆滿看 Java heap space,執行緒滿是 unable to create new native thread。前者加堆,後者查 OS 限制和執行緒數。
容器環境還多一層:宿主機 ulimit -u 放開了,但容器 cgroup 的 pids 限制沒調,照樣開不了執行緒。
小結
- 這個報錯是 OS 拒絕創建新原生執行緒,和 Java 堆無關。
- 根因:執行緒數頂到 ulimit/內核/容器限制,或執行緒棧耗光 native 記憶體。
- 先
ulimit -u加數執行緒定位,再決定調限制還是減執行緒。 - 治本是少開執行緒,或上虛擬執行緒。
- 別拿
-Xmx去修,方向錯了。
