天天看點

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

雖然這篇文章的标題打着jvm源碼分析的旗号,不過本文不僅僅從jvm源碼角度來分析,更多的來自于linux kernel的源碼分析,今天要說的是jvm裡比較常見的一個問題

這個問題可能有幾種表述

一個java程序到底能建立多少線程?

到底有哪些因素決定了能建立多少線程?

<code>java.lang.outofmemoryerror: unable to create new native thread</code>的異常究竟是怎麼回事

不過我這裡先聲明下可能不能完全百分百将各種因素都理出來,因為畢竟我不是做linux kernel開發的,還有不少細節沒有注意到的,我将我能分析到的因素和大家分享一下,如果大家在平時工作中還碰到别的因素,歡迎在文章下面留言,讓更多人參與進來讨論

線程大家都熟悉,<code>new thread().start()</code>即會建立一個線程,這裡我首先指出一點<code>new thread()</code>其實并不會建立一個真正的線程,隻有在調用了start方法之後才會建立一個線程,這個大家分析下java代碼就知道了,thread的構造函數是純java代碼,start方法會調到一個native方法start0裡,而start0其實就是<code>jvm_startthread</code>這個方法

從上面代碼裡首先要大家關注下最後的那個if判斷<code>if (native_thread-&gt;osthread() == null)</code>,如果osthread為空,那将會抛出大家比較熟悉的<code>unable to create new native thread</code> oom異常,是以osthread為空非常關鍵,後面會看到什麼情況下osthread會為空

另外大家應該注意到了<code>native_thread = new javathread(&amp;thread_entry, sz)</code>,在這裡才會真正建立一個線程

上面代碼裡的<code>os::create_thread(this, thr_type, stack_sz)</code>會通過<code>pthread_create</code>來建立線程,而linux下對應的實作如下:

如果在<code>new osthread</code>的過程中就失敗了,那顯然osthread為null,那再回到上面第一段代碼,此時會抛出<code>java.lang.outofmemoryerror: unable to create new native thread</code>的異常,而什麼情況下<code>new osthread</code>會失敗,比如說記憶體不夠了,而這裡的記憶體其實是c heap,而非java heap,由此可見從jvm的角度來說,影響線程建立的因素包括了xmx,maxpermsize,maxdirectmemorysize,reservedcodecachesize等,因為這些參數會影響剩餘的記憶體

另外注意到如果<code>pthread_create</code>執行失敗,那通過<code>thread-&gt;set_osthread(null)</code>會設定空值,這個時候osthread也為null,是以也會抛出上面的oom異常,導緻建立線程失敗,是以接下來要分析下<code>pthread_create</code>失敗的因素

pthread_create的實作在glibc裡,

上面我主要想說的一段代碼是<code>int err = allocate_stack (iattr, &amp;pd)</code>,顧名思義就是配置設定線程棧,簡單來說就是根據iattr裡指定的stacksize,通過mmap配置設定一塊記憶體出來給線程作為棧使用

那我們來說說stacksize,這個大家應該都明白,線程要執行,要有一些棧空間,試想一下,如果配置設定棧的時候記憶體不夠了,是不是建立肯定失敗?而stacksize在jvm下是可以通過-xss指定的,當然如果沒有指定也有預設的值,下面是jdk6之後(含)預設值的情況

估計不少人有一個疑問,棧記憶體到底屬于-xmx控制的java heap裡的部分嗎,這裡明确告訴大家不屬于,是以從glibc的這塊邏輯來看,jvm裡的xss也是影響線程建立的一個非常重要的因素。

如果棧配置設定成功,那接下來就要建立線程了,大概邏輯如下

而create_thread其實是調用的系統調用clone

系統調用這塊就切入到了linux kernel裡

clone系統調用最終會調用<code>do_fork</code>方法,接下來通過剖解這個方法來分析kernel裡還存在哪些因素

先看這麼一段,這裡其實就是判斷使用者的程序數有多少,大家知道在linux下,程序和線程其資料結構都是一樣的,是以這裡說的程序數可以了解為輕量級線程數,而這個最大值是可以通過<code>ulimit -u</code>可以查到的,是以如果目前使用者起的線程數超過了這個限制,那肯定是不會建立線程成功的,可以通過<code>ulimit -u value</code>來修改這個值

在這個過程中不乏有malloc的操作,底層是通過系統調用brk來實作的,或者上面提到的棧是通過mmap來配置設定的,不管是malloc還是mmap,在底層都會有類似的判斷

如果程序被配置設定的記憶體段超過<code>sysctl_max_map_count</code>就會失敗,而這個值在linux下對應<code>/proc/sys/vm/max_map_count</code>,預設值是65530,可以通過修改上面的檔案來改變這個門檻值

還存在<code>max_threads</code>的限制,代碼如下

如果要修改或者檢視可以通過<code>/proc/sys/kernel/threads-max</code>來操作,

這個值是受到實體記憶體的限制,在<code>fork_init</code>的時候就計算好了

pid也存在限制

而<code>alloc_pid</code>的定義如下

在<code>alloc_pidmap</code>中會判斷<code>pid_max</code>,而這個值的定義如下

這個值可以通過<code>/proc/sys/kernel/pid_max</code>來檢視或者修改

通過對jvm,glibc,linux kernel的源碼分析,我們暫時得出了一些影響線程建立的因素,包括

jvm:<code>xmx</code>,<code>xss</code>,<code>maxpermsize</code>,<code>maxdirectmemorysize</code>,<code>reservedcodecachesize</code>等

kernel:<code>max_user_processes</code>,<code>max_map_count</code>,<code>max_threads</code>,<code>pid_max</code>等

由于對kernel的源碼研讀時間有限,不一定總結完整,大家可以補充