1. 程式人生 > 實用技巧 >宇智波程式筆記23-探討一臺機器中JVM能建立的執行緒上限到底是多大

宇智波程式筆記23-探討一臺機器中JVM能建立的執行緒上限到底是多大

這兩天在用多執行緒ThreadPoolExecutor解決問題的時候,突發奇想的瞭解一下jvm到底最多能建立多少執行緒,因為在遇到高併發業務場景的時候,必須使用多執行緒來應付問題,正所謂兵來將擋,水來土掩,業務請求來自然就是執行緒幹活了.瞭解一下影響jvm建立執行緒的因素對後續jvm調優,高併發問題的解決多多少會有點幫助吧,,哪怕一點.

JVM 體系結構

要想了解jvm對執行緒的影響,首先得簡單瞭解一下jvm的體系結構,這裡直接上圖:

jvm的基本結構圖

上圖是從網上直接扒下來的,其實都差不多,簡單介紹一下;

(1) 程式計數器:

  這玩意又叫PC暫存器, 程式計數器是執行緒私有的記憶體,JVM多執行緒是通過執行緒輪流切換並分配處理器執行時間的方式實現的,當執行緒切換後需要恢復到正確的執行位置(處理器)時,就是通過程式計數器來實現的。此記憶體區域是唯一 一個在JVM規範中沒有規定任何OutOfMemoryError情況的區域。

(2) Java虛擬機器棧:

  Java虛擬機器棧也是執行緒私有的,它的生命週期與執行緒相同,Java虛擬機器棧為JVM執行的Java方法(位元組碼)服務。每個Java方法在執行時都會建立一個棧幀,用於儲存區域性變量表、運算元棧、動態連結串列、方法出口等資訊。

  區域性變量表存放的是基本資料型別,物件引用和returnAddress型別。也就是說基本資料型別直接在棧中分配空間;區域性變數(在方法或者程式碼塊中定義的變數)也在棧中分配空間,當方法執行完畢後該空間會被JVM回收;引用資料型別,即我們new建立的物件引用,JVM會在棧空間給這個引用分配一個地址空間(門牌號),在堆空間給該物件分配一個空間(家),棧空間的地址引用指向堆空間的物件(通過門牌號找到家)。在這個區域,JVM規範規定了兩個異常狀況:

   a.如果執行緒請求的棧深度大於JVM所允許的深度,將丟擲StackOverflowError異常;

   b.如果虛擬機器棧可以動態擴容(大部分JVM都可以動態擴容),如果擴充套件時無法申請到足夠的記憶體,就會丟擲OutOfMemoryError異常。

(3) 本地方法棧:

  和Java虛擬機器棧作用相似,只是本地方法棧為JVM使用到的Native(本地)方法服務,它也會丟擲StackOverflowError和OutOfMemoryError異常

(4) Java堆:

  JVM記憶體中最大的一塊,是所有執行緒共享的區域,在JVM啟動時建立,唯一目的就是用來儲存物件例項的,也被稱為GC堆,因為這是垃圾收集器

管理的主要區域。Java堆還可分為:新生代和老年代,其中新生代還可再分為:Eden:From Survivor:To Survivor = 8:1:1,廢話少說,直接上圖:

上圖結構很明顯的給我們展示了,影響jvm記憶體空間的幾個引數,簡單介紹下:

-Xmax:表示堆記憶體的最大值,預設(MaxHeapFreeRatio引數可以調整)空餘堆記憶體大於70%時,JVM會減少堆直到 -Xms的最小限制

-Xms:表示堆記憶體的初始大小,預設(MinHeapFreeRatio引數可以調整)空餘堆記憶體小於40%時,JVM就會增大堆直到-Xmx的最大限制.

-Xss:表示虛擬機器棧中的執行緒棧空間大小,JDK5.0以後每個執行緒堆疊大小為1M,以前每個執行緒堆疊大小為256K.更具應用的執行緒所需記憶體大小進行,這玩意將會直接影響執行緒的數量,接下來會繼續探討.

-XX:MaxNewSize:表示新生代最大空間

-XX:NewSize:表示新生代初始空間大小

-XX:MaxPermSize(jdk8以後改成了MaxMetaspaceSize) 表示永久代(元空間)空間大小

-XX:PermSize(jdk8以後改成了MetaspaceSize)表示永久代(元空間)初始空間大小

JVM執行緒影響因素

我們在使用ThreadPoolExcutor(jdk8建議使用此方法來建立多執行緒)建立多執行緒的時候,考慮到實際的業務場景以及伺服器的實際使用情況,建立執行緒的過程中多多少少會帶來如下常見問題:

(1) 執行緒的建立和銷燬,會帶來系統開銷.如果給每個任務都去啟動一個執行緒處理,那麼勢必造成記憶體以及cpu等資源的開銷

(2) 無限制的啟動過多執行緒,帶來最明顯的效果就是記憶體使用猛增,CPU排程高居不下,過多的執行緒佔用了超多的記憶體,以及其他內部資源,導致jvm的GC壓力增大,CPU排程不及時,如果執行緒數量超過了底層OS可處理的數量,直接影響到整個系統的效能

因此,jvm能夠建立的執行緒總數,除了系統分配給jvm的內部引數以外,平臺本身的資源情況也會影響到執行緒的總數,也即底層作業系統對執行緒做了限制.

我們先來驗證一下jvm引數,在不考慮底層OS對執行緒的限制情況下,所能建立的最大執行緒數是多少,這裡直接貼一下我寫的程式碼段,大家可以直接拿下來執行:

程式碼中的statckSize可以自定義,也可以直接修改-Xss來定義


public class Test {

    private int depth ;

    private void recur(){
        this.depth++;
        recur();
    }

    private void getStackDepth(){
        try
        {

            recur();

        }catch (Throwable t ){
            System.out.println("得到的棧最大深度為\t"+this.depth);
            t.printStackTrace();
        }
    }


    public static void main(String[] args) {

        long stackSize = Long.parseLong(args[0]) ;

        for (int i=1;i<Integer.MAX_VALUE;i++){

            try {
                int tmp = i ;
                MyThreadFactory.getInstance(tmp,stackSize).newThread(() ->{
                    try {
                        Field field = Thread.class.getDeclaredField("stackSize");
                        field.setAccessible(true);
                        long stackSize1 = field.getLong(Thread.currentThread());
                        System.out.println("執行緒棧記憶體大小"+stackSize1+"\t當前OS預設棧記憶體大小為"+stackSize/1024+"\t"+Thread.currentThread().getName()+"started...");
                        Thread.sleep(Long.MAX_VALUE);
                    } catch (Exception e) {
                        throw new RuntimeException();
                    }

                }).start();

            }catch (Throwable ex ){
                System.out.println("支援的最大執行緒數為\t"+i);
                ex.printStackTrace();
                break;
            }

        }

    }


    private static class MyThreadFactory implements ThreadFactory {

        private  static int num ;

        private static long stackSize ;

        private static final AtomicInteger poolAtomic = new AtomicInteger(1);

        private static final AtomicInteger threadAtomic = new AtomicInteger(1);


        private ThreadGroup threadGroup  ;

        private static MyThreadFactory myThreadFactory ;

        private String threadName ;

        public MyThreadFactory(){
            SecurityManager securityManager = System.getSecurityManager() ;
            threadGroup = securityManager==null?Thread.currentThread().getThreadGroup():securityManager.getThreadGroup();
            threadName = "pool-"+poolAtomic.getAndIncrement()+"-thread-";

        }


        public static MyThreadFactory getInstance(int i,long size  ){
            myThreadFactory = new MyThreadFactory() ;
            num = i ;
            stackSize=size;
            return myThreadFactory ;

        }



        @Override
        public Thread newThread(Runnable r) {

            Thread thread = new Thread(threadGroup,r,threadName+num,stackSize);
            if(thread.isDaemon()){
                thread.setDaemon(false);
            }

            if(thread.getPriority() != Thread.NORM_PRIORITY){
                thread.setPriority(Thread.NORM_PRIORITY);
            }

            return thread;

        }
    }



}

為了便於監測執行情況,我將原始碼釋出在了VMware虛擬機器上(直接使用本機物理機也行,如果不怕被搞死的話),通過cm的方式直接執行,檢視結果.

不考慮硬體資源

驗證java -Xmx512m -Xms512m -Xss228k com/inspur/x1/office/utils/Test 0 > 1.log

執行完畢,發現出現unable to create new native thread 的錯誤日誌 ,看一下1.log日誌,發現開啟的最大執行緒數為:27643

驗證 java -Xmx1024m -Xms1024m -Xss228k com/inspur/x1/office/utils/Test 0 > 2.log

執行完畢,發現出現unable to create new native thread 的錯誤日誌 ,看一下2.log日誌,發現開啟的最大執行緒數為:27639

驗證 java -Xmx2048m -Xms2048m -Xss228k com/inspur/x1/office/utils/Test 0 > 3.log

執行完畢,發現出現unable to create new native thread 的錯誤日誌 ,看一下3.log日誌,發現開啟的最大執行緒數為:18728

驗證java -Xmx512m -Xms512m -Xss2048k com/inspur/x1/office/utils/Test 0 > 4.log

執行完畢,發現出現unable to create new native thread 的錯誤日誌,並且記憶體全部被吃掉,free區變成了0,看一下4.log日誌,發現開啟的最大執行緒數為:24910

驗證java -Xmx512m -Xms512m com/inspur/x1/office/utils/Test 2097152> 5.log

執行完畢,發現出現unable to create new native thread 的錯誤日誌,並且記憶體全部被吃掉,free區變成了0,看一下5.log日誌,發現開啟的最大執行緒數為:27638

說明一下:

上述資源在執行的時候,無論將Xms Xmx 以及Xss 設定多大,在記憶體足夠的情況下,可建立的最大執行緒數永遠不會超過27834,因為這是硬體決定的

考慮硬體資源

如果我想讓執行緒數達到100000量級,需要修改如下幾個系統資源引數

1)/proc/sys/kernel/pid_max

此引數定義了OS最大能支援的程序數,與使用者態不同,對於Linux核心而言,程序和執行緒之間的區別並不大,執行緒也不過是共享記憶體空間的程序。每個執行緒都是一個輕量級程序(Light Weight Process),都有自己的唯一PID(或許叫TID更合適一些)和一個TGID(Thread group ID),TGID是啟動整個程序的thread的PID.\

執行命令 ps -fL

圖中LWP表示輕量級的程序,當啟動多個執行緒的時候,LWP值會增加,一直增加到OS所能支援的最大值為止

2)/proc/sys/kernel/thread-max

此引數限制了OS所能支援的執行緒最大值,預設值為如下圖所示

3)/proc/sys/vm/max_map_count

定義程式執行的時候,在記憶體共享區域建立的VMA(虛擬記憶體區域),描述的是程式在分配記憶體空間的時候,會建立該區域,因此,值越大,分配的程序VMA越多,

這個值直接影響程序擁有的VMA的數量,擁有的越多,程序將會越多,也越佔記憶體,進而導致系統報出記憶體不足的異常,預設值為如下圖所示

4) max_user_process 當前使用者允許的最大程序數,預設值如下圖,正好是thread-max的一半,(兩者具體的聯絡,目前沒搞清楚,後續再研究)

如果想客服硬體資源的限制,在現有資源的基礎上,得到100000併發請求,需要如下配置:

1) 設定 thread-max 為100000

2) 設定 max_map_count 為100000

3) 設定 max_user_process 為unlimited

執行 java -Xmx512m -Xms512m com/inspur/x1/office/utils/Test 2097152> 6.log

看到執行完畢之後,發現執行緒數量猛增到接近50000的數量,將max_map_count 值設定成200000試試

執行緒總數飄上來了,接近100000併發,由於記憶體資源被其他程序佔據了一部分,已經很接近目標了,,

執行以下 jstat -gc 程序id,看看當前堆記憶體的生存情況,發現eden區與survivor區的記憶體基本全用上了

並且記憶體已經全部佔滿了,這就是max-map-count的作用

總結

jvm可建立的最大執行緒數,最根本的是受到OS底層資源的限制,而執行緒數可分配的數量取決於Xms Xmx 以及Xss引數的配置,但上限不會超過OS硬體支援的總數,硬體資源受到thread-max,max_user_process以及max_map_count的限制,總結成一個公式就是:

最大可用執行緒數=(OS最大程序記憶體-JVM記憶體-系統保留記憶體)/單個執行緒棧空間大小

func squareOf(x: Int) -> Int {return x * x}

func divideTenBy(x: Int) throws -> Double {
    guard x != 0 else {
        throw CalculationError.DivideByZero
    }
    return 10.0 / Double(x)
}

let theNumbers = [10, 20, 30]
let squareResult = theNumbers.map(squareOf(x:)) // [100, 400, 9000]

do {
    let divideResult = try theNumbers.map(divideTenBy(x:))
} catch let error {
    print(error)
using TickEvent = std::function<void(std::int64_t elapsed_ms)>;
  
  using TickRunCallback = std::function<void()>
  
  using clock = std::chrono::high_resolution_clock;
  
  Tick(std::int64_www.jintianxuesha.com t tick_ms,
  
  std::int64_t life_ms =www.hengxinzhce.cn std::numeric_limits<std::int64_t>::max());
  
  Tick(TickEvent tick_event, www.feihongyul.cn std::int64_t tick_ms,
  
  std::int64_t life_ms = std::numeric_limits<www.uuedzc.cn std::int64_t>::max(),
  
  TickRunCallback run_beg = nullptr,
  
  TickRunCallback run_end = nullptr);
  
  virtual ~Tick(www.qiaoheibpt.com )www.jujinyule.com ;
  
  bool IsRunning(www.yachengyl.cn) const;
  
  void Start(www.baichuangyule.cn);
  
  void Stop(bool wait_life_over = false);
  
  const std::chrono::www.jinmazx.cn www.bhylzc.cn time_point<clock> &GetTimeStart() const;
  
  void SetTickEvent(TickEvent &&tick_event);
  
  void SetTickEvent(www.zhuyngyule.cn const TickEvent &tick_event);
  
  void SetRunBegCallback(www.shicaiyulezc.cn www.wyuleezc.cn TickRunCallback &&run_beg);
  
  void SetRunBegCallback(www.pingguoyul.cn www.kunlunyxgw.com const TickRunCallback &run_beg);
  
  void SetRunEndCallback(TickRunCallback &&run_end);
  
  void SetRunEndCallback(const TickRunCallback &run_end);

舉個例子:

對於棧大小為512KB(即stackSize=512K Xss256k)的jdk1.8而言(拋開硬體資源限制):
1GB allocated to JVM: ~ 大概20000 - 23000 threads
2GB allocated to JVM: ~ 大概 150000 - 18000threads

可以看到,分給heap的記憶體越小,理論上得到的執行緒數就越多,反之越少

ok,以上是本人親測總結的影響jvm建立執行緒受到的影響因素,有不正確的地方歡迎批評指正!