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

本文來自: PerfMa技術社區

PerfMa(笨馬網絡)官網java

概述

雖然這篇文章的標題打着JVM源碼分析的旗號,不過本文不只僅從 JVM 源碼角度來分析,更多的來自於 Linux Kernel 的源碼分析,今天要說的是 JVM 裏比較常見的一個問題。linux

這個問題可能有幾種表述網絡

  • 一個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這個方法。jvm

1.jpg

從上面代碼裏首先要你們關注下最後的那個 if 判斷 if (native_thread->osthread() == NULL),若是 osthread 爲空,那將會拋出你們比較熟悉的 unable to create new native thread OOM 異常,所以 osthread 爲空很是關鍵,後面會看到什麼狀況下osthread會爲空。函數

另外你們應該注意到了native_thread = new JavaThread(&thread_entry, sz),在這裏纔會真正建立一個線程。源碼分析

2.jpg

上面代碼裏的os::create_thread(this, thr_type, stack_sz)會經過pthread_create來建立線程,而 Linux 下對應的實現以下:學習

3.jpg

4.jpg

5.jpg

若是在 new OSThread 的過程當中就失敗了,那顯然 osthread 爲 NULL,那再回到上面第一段代碼,此時會拋出java.lang.OutOfMemoryError: unable to create new native thread的異常,而什麼狀況下new OSThread會失敗,好比說內存不夠了,而這裏的內存實際上是 C Heap,而非 Java Heap,因而可知從 JVM 的角度來講,影響線程建立的因素包括了 Xmx,MaxPermSize,MaxDirectMemorySize,ReservedCodeCacheSize 等,由於這些參數會影響剩餘的內存this

另外注意到若是pthread_create執行失敗,那經過thread->set_osthread(NULL)會設置空值,這個時候 osthread 也爲 NULL,所以也會拋出上面的 OOM 異常,致使建立線程失敗,所以接下來要分析下 pthread_create 失敗的因素。spa

glibc 中的 pthread_create

stack_size

pthread_create 的實如今 glibc 裏,

6.jpg

上面我主要想說的一段代碼是int err = ALLOCATE_STACK (iattr, &pd),顧名思義就是分配線程棧,簡單來講就是根據 iattr 裏指定的 stackSize,經過 mmap 分配一塊內存出來給線程做爲棧使。

那咱們來講說 stackSize,這個你們應該都明白,線程要執行,要有一些棧空間,試想一下,若是分配棧的時候內存不夠了,是否是建立確定失敗?而 stackSize 在 JVM 下是能夠經過 -Xss 指定的,固然若是沒有指定也有默認的值,下面是 JDK6 以後(含)默認值的狀況。

Linux Kernel 裏的 clone

若是棧分配成功,那接下來就要建立線程了,大概邏輯以下

7.jpg

而create_thread實際上是調用的系統調用clone

8.jpg

系統調用這塊就切入到了 Linux Kernel 裏

clone 系統調用最終會調用do_fork方法,接下來經過剖解這個方法來分析 Kernel 裏還存在哪些因素。

max_user_processes

9.jpg

先看這麼一段,這裏其實就是判斷用戶的進程數有多少,你們知道在linux下,進程和線程其數據結構都是同樣的,所以這裏說的進程數能夠理解爲輕量級線程數,而這個最大值是能夠經過ulimit -u能夠查到的,因此若是當前用戶起的線程數超過了這個限制,那確定是不會建立線程成功的,能夠經過ulimit -u value來修改這個值

max_map_count

在這個過程當中不乏有 mallo c的操做,底層是經過系統調用 brk 來實現的,或者上面提到的棧是經過 mmap 來分配的,不論是 malloc 仍是 mmap,在底層都會有相似的判斷。

10.jpg

若是進程被分配的內存段超過sysctl_max_map_count就會失敗,而這個值在 linux 下對應/proc/sys/vm/max_map_count,默認值是 65530,能夠經過修改上面的文件來改變這個閾值

max_threads

還存在max_threads的限制,代碼以下:

11.jpg

若是要修改或者查看能夠經過/proc/sys/kernel/threads-max來操做, 這個值是受到物理內存的限制,在fork_init的時候就計算好了。

12.jpg

pid_max

pid 也存在限制

13.jpg

alloc_pid的定義以下

14.jpg

alloc_pidmap中會判斷pid_max,而這個值的定義以下

15.jpg

這個值能夠經過 /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 的源碼研讀時間有限,不必定總結完整,你們能夠補充。

一塊兒來學習吧

PerfMa KO 系列課之 JVM 參數【Memory篇】

jvm堆內存溢出後,其餘線程是否可繼續工做

相關文章
相關標籤/搜索