• 周六. 9 月 14th, 2024

    架构师之路(十一)之探讨一台机器中JVM能创建的线程上限到底是多大?

    root

    2 月 21, 2021 #架构师之路

    引言 

    这两天在用多线程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内存-系统保留内存)/单个线程栈空间大小

    举个例子:

    对于栈大小为512KB(即stackSize=512K Xss256k)的jdk1.8而言(抛开硬件资源限制):
    1GB allocated to JVM: ~ 大概20000 – 23000 threads
    2GB allocated to JVM: ~ 大概 150000 – 18000 threads

    可以看到,分给heap的内存越小,理论上得到的线程数就越多,反之越少

       ok,以上是本人亲测总结的影响jvm创建线程受到的影响因素,有不正确的地方欢迎批评指正!

    root