对Java Stack的一次探索

原创
2018/10/31 17:45
阅读数 1.9K

问题说明

昨天发现线上有一些业务逻辑没有执行到,但是代码入口代码日志已经打印,深入下去一看,底层库里有一个事件执行的方法在每次执行时都会 new 一个 thread,在以往量不大时没有问题,量大时就可能导致线程创建不出来,报OOM错误(由于有同事在我看这个时重启了服务导致 gc 日志被清空和栈信息丢失,这个原因只是一个猜测)。

由此让我好奇几个问题

  1. Java 最多可以创建多少线程?
  2. Java 控制线程大小选项 -Xss 的具体含义是什么?
  3. Java 的选项 -Xmx -Xms 控制堆的选项对线程创建有无影响?
  4. Java 的线程具体是怎么实现的?

探索

其实这几个问题是相互交错的,在查询过程中很多答案对这几个问题都有涉及,因此下面很多链接并不仅仅是针对某一个问题,更是一个一般的描述。

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 的值对线程创建也有影响,如果太小在创建太多线程后会报错:

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,剩余内存13G

测试程序:

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 打满:

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

展开阅读全文
打赏
0
1 收藏
分享
加载中
更多评论
打赏
0 评论
1 收藏
0
分享
返回顶部
顶部