架构师之路(十一)之探讨一台机器中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内存-系统保留内存)/单个线程栈空间大小
举个例子:
对于栈大小为512KB(即stackSize=512K Xss256k)的jdk1.8而言(抛开硬件资源限制):
1GB allocated to JVM: ~ 大概20000 - 23000 threads
2GB allocated to JVM: ~ 大概 150000 - 18000 threads
可以看到,分给heap的内存越小,理论上得到的线程数就越多,反之越少
ok,以上是本人亲测总结的影响jvm创建线程受到的影响因素,有不正确的地方欢迎批评指正!

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
FTP服务器 打包 下载文件
需求:从ftp服务器打包下载文件 解决步骤:1.从ftp服务器把各个文件下载到本地服务器(一般是安装tomcat的服务器,项目自己电脑跑的本地服务器就是自己电脑)指定目录中 2.在本地服务器打包下载好的文件夹打包,返回打包好的File zip 3.zip文件用流写入reponse,达到用户下载效果 准备文件: // 封装所有需要打包下载的文件地址请求类 public class DownloadPackageReq implements Serializable { // 本地服务器临时存放目录名(尽量唯一.eg:"menutree20200904112125") private String localTempDirName; // 打包下载本地服务器文件夹名字 private List<DownloadPackageListReq> downloadPackageListReqList; // 需要下载所有文件路径和名称 } // FTPClientUtils:ftp工具类 public static FTPClientUtils init() { FTP...
- 下一篇
SQL注入理解
SQL注入理解 1. 定义/类型 定义:简单来说,当客户端提交的数据未做处理或转义直接带入数据库就造成了SQL注入。 注入类型分为: 1. 整型(没有单双引号) 2. 字符串(有单双引号) 3. 其他细分的类型本质上就是整型和字符串的区别 2.联合注入 判断整型注入还是字符型注入 and 1=2 //页面正常-->不是整型注入 id=1' //加单引号,页面不正常,字符型注入 --+ 将后面的语句注释掉,页面正常,判断为单引号注入 获取字段总数 ‘ order by 3 --+ group by 3 //判断字段总数是否>=3 union连接查询(字段必须和表格字段总数相符) select * from admin where id=1 union select 1,1,1; //union select 字段,字段,字段 select username,password from admin where id=1 union select 1,1; //union前后字段对应 获取数据库信息 爆破数据库名称 http://127.0.0.1/sqli-labs/Less-...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS8编译安装MySQL8.0.19
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Mario游戏-低调大师作品
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- Linux系统CentOS6、CentOS7手动修改IP地址
- CentOS8安装Docker,最新的服务器搭配容器使用
- CentOS7安装Docker,走上虚拟化容器引擎之路