對Java Stack的一次探索

問題說明

昨天發現線上有一些業務邏輯沒有執行到,可是代碼入口代碼日誌已經打印,深刻下去一看,底層庫裏有一個事件執行的方法在每次執行時都會 new 一個 thread,在以往量不大時沒有問題,量大時就可能致使線程建立不出來,報OOM錯誤(因爲有同事在我看這個時重啓了服務致使 gc 日誌被清空和棧信息丟失,這個緣由只是一個猜想)。html

由此讓我好奇幾個問題java

  1. Java 最多能夠建立多少線程?
  2. Java 控制線程大小選項 -Xss 的具體含義是什麼?
  3. Java 的選項 -Xmx -Xms 控制堆的選項對線程建立有無影響?
  4. Java 的線程具體是怎麼實現的?

探索

其實這幾個問題是相互交錯的,在查詢過程當中不少答案對這幾個問題都有涉及,所以下面不少連接並不單單是針對某一個問題,更是一個通常的描述。linux

Java 虛擬機運行於 Linux服務器上,所以第一個問題和第四個問題能夠合在一塊兒看。Java 線程直接map的 OS 的 native thread[^1] [^2], 所以Linux 對線程的限制也就限制了 Java 能夠建立的線程。Linux 對系統能建立的總的線程數和每一個用戶可以建立的線程數都是有限制的。
操做系統總的的限制能夠看 /proc/sys/kernel/pid_max 值[^4],/proc/sys/kernel/threads-max [^5] 值, /proc/sys/vm/max_map_count 的值對線程建立也有影響,若是過小在建立太多線程後會報錯:centos

OpenJDK 64-Bit Server VM warning: Attempt to protect stack guard pages failed.
OpenJDK 64-Bit Server VM warning: Attempt to allocate stack guard pages failed.
OpenJDK 64-Bit Server VM warning: Attempt to allocate stack guard pages failed.
OpenJDK 64-Bit Server VM warning: Attempt to deallocate stack guard pages failed.
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
        at java.lang.Thread.start0(Native Method)
        at java.lang.Thread.start(Thread.java:691)
        at ThreadTest.main(ThreadTest.java:6)
OpenJDK 64-Bit Server VM warning: Attempt to deallocate stack guard pages failed.
OpenJDK 64-Bit Server VM warning: Attempt to allocate stack guard pages failed.

其餘用戶、組等限制能夠在 /etc/security/limits.conf 中查看,用戶的限制也可使用 ulimit -u 來查看數組

對於第二個問題,-Xss 的具體含義模糊點在於之前一直覺得這是限制每一個線程可以使用的棧的最大值,可是在查問題過程當中看到有一個網友回答How does Java (JVM) allocate stack for each thread 裏面提到The minimum stack size in HotSpot for a thread seems to be fixed. This is what the aforementioned -Xss option is for.,這句話應該是不對的,Xss 限制的就是線程棧的最大值。所以接下來就是這個棧大小是動態擴展的仍是線程建立時就直接分配好的呢?根據 Java Spec [^3],這兩種方式根據實現決定,不過按照實驗來看,棧大小應該是動態擴展的。服務器

第三個問題,-Xmx-Xms 決定了Java 使用堆的大小,一直有人說將二者設爲同樣大小可讓Java 在啓動時就分配好,能夠防止後續堆的抖動,但就我實驗來看,堆並無在一開始就分配了,選項這樣設置應該只能控制堆能夠分配的最大值,堆容量分配後就再也不縮小(防止抖動效果)。所以堆大小會影響線程數量,但前提是堆已經被分配,若是堆一直沒有使用,內存不會爲堆保留。多線程

測試

服務器配置:
OS : centos 6.4
jdk : java version "1.7.0_09-icedtea"
OpenJDK Runtime Environment (rhel-2.3.4.1.el6_3-x86_64)
OpenJDK 64-Bit Server VM (build 23.2-b09, mixed mode)
內存:總16G,剩餘內存13Goracle

測試程序:jvm

public class ThreadTest {
    public static void main(String[] args) {
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            System.out.println("create " + i + "th thread");
            try {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            while (true) {
                                Thread.sleep(100);
                            }
                        } catch (Exception e) {

                        }
                    }
                }).start();
            } catch (StackOverflowError stackOverflowError) {
                System.out.println("create " + i + "th thread error, stackOverflow");
                stackOverflowError.printStackTrace();
            } catch (Exception e) {
                System.out.println("create " + i + "th thread error");
                e.printStackTrace();
                break;
            }
        }
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                break;
            }
        }
    }
}
  1. 未修改pid_max(32768), max_map_count等數值
    1. 執行 java -Xmx8096m -Xms8096m -Xss1m ThreadTest,顯示建立了 31341後報OOM,不過這個值是變化的,但一直在31.3K範圍中,內存剩餘不少。
    2. 執行 java -Xmx10096m -Xms10096m -Xss1m ThreadTest,顯示建立線程也在31K處報OOM
    3. 執行 java -Xmx8096m -Xms8096m -Xss10m ThreadTest 結果同樣
  2. 修改pid_max(1000000), max_map_count(1000000)
    1. 執行 java -Xmx8096m -Xms8096m -Xss1m ThreadTest,一直在建立,可是在到70多w的時候會一直卡,整個系統接近不響應,但一直關注內存還剩3-4G

更新程序將heap 打滿:ide

public class ThreadTest {
    public static void main(String[] args) {
        
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            System.out.println("create " + i + "th thread");
            try {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        byte[] heap = new byte[1024 * 1024];//1M
                        try {
                            while (true) {
                                Thread.sleep(100);
                            }
                        } catch (Exception e) {

                        }
                        System.out.println(heap[0]);
                    }
                }).start();
            } catch (StackOverflowError stackOverflowError) {
                System.out.println("create " + i + "th thread error, stackOverflow");
                stackOverflowError.printStackTrace();
            } catch (Exception e) {
                System.out.println("create " + i + "th thread error");
                e.printStackTrace();
                break;
            }
        }
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                break;
            }
        }
        
    }
}

執行 java -Xmx12096m -Xms12096m -Xss1m -server ThreadTest, 程序建立到耗盡heap 內存才報OOM:heap size,分配heap數組時報異常會致使線程退出,而後又繼續建立線程。

因爲java的大對象都分配在堆上,所以沒什麼好辦法耗盡棧內存,但能夠看出棧在初始化時是很小的,更大的影響因素仍是Linux的線程數限制。

結論:

  1. Java 線程建立取決於操做系統限制(pid_max, max_map_count, memory等)
  2. stack 的棧幀是動態分配的,-Xss 限制棧最大值
  3. -Xmx -Xms 限制堆大小,與棧共用內存,是相互影響的。

Reference

[^1]: Light-Weight Processes: Dissecting Linux Threads

[^2]: Distinguishing between Java threads and OS threads?

[^3]: 2.5.2. Java Virtual Machine Stacks

[^4]: If threads share the same PID, how can they be identified?

[^5]: Maximum number of threads that can be created within a process in C

相關文章
相關標籤/搜索