JVM源碼分析之一個Java進程究竟能創建多少線程

概述
雖然這篇文章的標題打着JVM源碼分析的旗號,不過本文不僅僅從JVM源碼角度來分析,更多的來自於Linux Kernel的源碼分析,今天要說的是JVM里比較常見的一個問題
這個問題可能有幾種表述
-
一個Java進程到底能創建多少線程?
-
到底有哪些因素決定了能創建多少線程?
-
java.lang.OutOfMemoryError: unable to create new native thread
的異常究竟是怎么回事
不過我這里先聲明下可能不能完全百分百將各種因素都理出來,因為畢竟我不是做Linux Kernel開發的,還有不少細節沒有注意到的,我將我能分析到的因素和大家分享一下,如果大家在平時工作中還碰到別的因素,歡迎在文章下面留言,讓更多人參與進來討論
從JVM說起
線程大家都熟悉,new Thread().start()
即會創建一個線程,這里我首先指出一點new Thread()
其實並不會創建一個真正的線程,只有在調用了start方法之后才會創建一個線程,這個大家分析下Java代碼就知道了,Thread的構造函數是純Java代碼,start方法會調到一個native方法start0里,而start0其實就是JVM_StartThread
這個方法
從上面代碼里首先要大家關注下最后的那個if判斷
if (native_thread->osthread() == NULL)
,如果osthread為空,那將會拋出大家比較熟悉的unable to create new native thread
OOM異常,因此osthread為空非常關鍵,后面會看到什么情況下osthread會為空
另外大家應該注意到了native_thread = new JavaThread(&thread_entry, sz)
,在這里才會真正創建一個線程
上面代碼里的os::create_thread(this, thr_type, stack_sz)
會通過pthread_create
來創建線程,而Linux下對應的實現如下:
如果在
new OSThread
的過程中就失敗了,那顯然osthread為NULL,那再回到上面第一段代碼,此時會拋出java.lang.OutOfMemoryError: unable to create new native thread
的異常,而什么情況下new OSThread
會失敗,比如說內存不夠了,而這里的內存其實是C Heap,而非Java Heap,由此可見從JVM的角度來說,影響線程創建的因素包括了Xmx,MaxPermSize,MaxDirectMemorySize,ReservedCodeCacheSize等,因為這些參數會影響剩余的內存
另外注意到如果pthread_create
執行失敗,那通過thread->set_osthread(NULL)
會設置空值,這個時候osthread也為NULL,因此也會拋出上面的OOM異常,導致創建線程失敗,因此接下來要分析下pthread_create
失敗的因素
glibc中的pthread_create
stack_size
pthread_create的實現在glibc里,
上面我主要想說的一段代碼是
int err = ALLOCATE_STACK (iattr, &pd)
,顧名思義就是分配線程棧,簡單來說就是根據iattr里指定的stackSize,通過mmap分配一塊內存出來給線程作為棧使用
那我們來說說stackSize,這個大家應該都明白,線程要執行,要有一些棧空間,試想一下,如果分配棧的時候內存不夠了,是不是創建肯定失敗?而stackSize在JVM下是可以通過-Xss指定的,當然如果沒有指定也有默認的值,下面是JDK6之后(含)默認值的情況
估計不少人有一個疑問,棧內存到底屬於-Xmx控制的Java Heap里的部分嗎,這里明確告訴大家不屬於,因此從glibc的這塊邏輯來看,JVM里的Xss也是影響線程創建的一個非常重要的因素。
Linux Kernel里的clone
如果棧分配成功,那接下來就要創建線程了,大概邏輯如下
而create_thread其實是調用的系統調用clone
系統調用這塊就切入到了Linux Kernel里
clone系統調用最終會調用do_fork
方法,接下來通過剖解這個方法來分析Kernel里還存在哪些因素
max_user_processes
先看這么一段,這里其實就是判斷用戶的進程數有多少,大家知道在linux下,進程和線程其數據結構都是一樣的,因此這里說的進程數可以理解為輕量級線程數,而這個最大值是可以通過ulimit -u
可以查到的,所以如果當前用戶起的線程數超過了這個限制,那肯定是不會創建線程成功的,可以通過ulimit -u value
來修改這個值
max_map_count
在這個過程中不乏有malloc的操作,底層是通過系統調用brk來實現的,或者上面提到的棧是通過mmap來分配的,不管是malloc還是mmap,在底層都會有類似的判斷
如果進程被分配的內存段超過sysctl_max_map_count
就會失敗,而這個值在linux下對應/proc/sys/vm/max_map_count
,默認值是65530,可以通過修改上面的文件來改變這個閾值
max_threads
還存在max_threads
的限制,代碼如下
如果要修改或者查看可以通過
/proc/sys/kernel/threads-max
來操作, 這個值是受到物理內存的限制,在fork_init
的時候就計算好了
pid_max
pid也存在限制
而
alloc_pid
的定義如下
在alloc_pidmap
中會判斷pid_max
,而這個值的定義如下
這個值可以通過
/proc/sys/kernel/pid_max
來查看或者修改
總結
通過對JVM,glibc,Linux kernel的源碼分析,我們暫時得出了一些影響線程創建的因素,包括
-
JVM:
Xmx
,Xss
,MaxPermSize
,MaxDirectMemorySize
,ReservedCodeCacheSize
等 -
Kernel:
max_user_processes
,max_map_count
,max_threads
,pid_max
等
由於對kernel的源碼研讀時間有限,不一定總結完整,大家可以補充
PS:今天正巧我們公司有個專門做性能的同事開通了公眾號並寫了一篇因為網絡抖動導致的性能問題有興趣的可以掃描二維碼關注一下,后續估計也會寫不少性能相關的文章