您现在的位置是:首页 > 文章详情

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

日期:2020-09-04点击:582

引言 

这两天在用多线程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创建线程受到的影响因素,有不正确的地方欢迎批评指正!

 

 

 

原文链接:https://my.oschina.net/yaukie/blog/4546539
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章