JVM能够开启多少线程
最近在看<<Java并发编程实战>>一书过程中,在Task Execution一节中讲到,针对每个任务都启动一个线程来处理,尤其在大量创建线程的场景下,会给工程实践带来很多问题.
1)线程的创建和销毁都是有开销的。线程的创建需要时间,如果针对每个任务都启动线程处理,则无疑会造成请求不能得到尽快处理。如果请求比较频繁而且轻量级,为每个请求创建新线程会消耗大量资源。
2)资源消耗.线程会消耗系统资源,尤其是内存。如果可运行的线程(Runnable状态)数量大于可用处理器数量,那么有些线程会得不到调度。这些线程一旦数量太多就会占用大量内存,给GC带来回收压力。同时大量的线程对CPU的竞争带来的上下文切换也会影响系统性能。如果已经有足够的线程来让CPU保持忙碌,那么再创建新的线程非但不能提高CPU利用率,反而会降低性能。
3)稳定性。能够创建的线程数量是有上限的。这个数量与平台相关,也受限于多个因素:JVM启动参数(-Xss),创建Thread时在构造函数中指定的线程栈大小(stack size)和底层操作系统对线程数的限制。
从第三点可见,线程数量和线程栈大小(通过-Xss或stack size指定,一般为0.5M-1M)、可用内存和操作系统限制有关系。因此本文讨论一下,究竟JVM最多能够开启多少线程。
不考虑OS限制的情况下,RAM越大,线程栈越小,可以创建的线程就越多。一个理论上的计算公式如下:
Max number of threads(最大线程数)=(进程最大可用内存-JVM分配内存-OS Reserved Memory(JNI,本地方法栈))/thread stack size(线程栈大小)
对于32位操作系统,允许创建的线程数量一个重要的制约因素便是内存RAM。由于寻址空间仅仅有2^32byte(4GB),按照线程栈大小为0.5M来计算,最大线程数为2^13,即不到1万。如果按照线程栈大小为1M来计算,最大线程数不过数千。
对于64为操作系统,内存不再是最大制约因素,最大线程数受限于Linux内核.以我的Ubuntu 64-bit 操作系统,16GB RAM为例,执行如下代码:
-
package snow.rabbit.java.thread;
-
/**
-
*
-
* @author lawrence
-
*
-
*/
-
public
class TestMaxSupportThreads {
-
-
private
static
final
class TestThread extends Thread
-
{
-
private
final
int number;
-
-
public TestThread(int number) {
-
this.number = number;
-
}
-
-
public TestThread(int number,int stackSize) {
-
super(
null,
null,
"Thread-Hi-" +number,stackSize);
-
this.number = number;
-
}
-
-
@Override
-
public void run() {
-
if (number%
100 ==
0)
-
{
-
System.out.println(
"Thread "+number+
" started.");
-
}
-
try {
-
Thread.sleep(Long.MAX_VALUE);
-
}
catch (InterruptedException e) {
-
e.printStackTrace();
-
}
-
}
-
}
-
/**
-
* @param args
-
*/
-
public static void main(String[] args) {
-
int i=
0;
-
try {
-
for (i=
0; i <
200; i++)
-
{
-
Thread t=
new TestThread(i,
2<<
19);
//512K
-
t.start();
-
}
-
}
catch (Throwable e) {
-
System.out.println(
"Error occured when creating thread "+i);
-
e.printStackTrace();
-
}
-
}
-
}
输出结果如下:
Thread 32100 started.
Error occured when creating thread 32129
java.lang.OutOfMemoryError: unable to create new native thread
at java.lang.Thread.start0(Native Method)
at java.lang.Thread.start(Thread.java:640)
at TestMaxSupportThreads.main(TestMaxSupportThreads.java:37)
此时内存还有很大剩余,但是线程创建到32129时便出现异常,这是由于OS的限制导致,和kernal/ulimit有关。具体见如下设置:
1)/proc/sys/kernel/pid_max :系统允许的最大pid
与用户态不同,对于Linux内核而言,进程和线程之间的区别并不大,线程也不过是共享内存空间的进程。每个线程都是一个轻量级进程(Light Weight Process),都有自己的唯一PID(或许叫TID更合适一些)和一个TGID(Thread group ID),TGID是启动整个进程的thread的PID.
简单来说,当一个进程被创建的时候,它其实是一个PID和TGID数值相同线程。当线程A启动线程B时,线程B会有自己的唯一PID,但它的TGID会从A继承而来。这样通过PID线程可以独立得到调度,而相同的TGID可以知道哪些线程属于同一个进程,这样可以共享资源(RAM,虚拟内存、文件等)。
通过ps -fL可以看到LWP轻量级进程信息:
lizhong@lizhongpc:~$ ps -fL
UID PID PPID LWP C NLWP STIME TTY TIME CMD
lizhong 4167 2252 4167 0 1 14:49 pts/2 00:00:00 -bash
lizhong 6592 4167 6592 0 1 16:40 pts/2 00:00:00 ps -fL
我们启动200个线程后,可以看到LWP显著增加。
lizhong@lizhongpc:~$ ps -fL
UID PID PPID LWP C NLWP STIME TTY TIME CMD
lizhong 4167 2252 4167 0 1 14:49 pts/2 00:00:00 -bash
lizhong 6828 4167 6828 0 1 16:40 pts/2 00:00:00 ps -fL
2)/proc/sys/kernel/threads-max :系统支持的最大线程数
3)max_user_process(ulimit -u):每个用户允许的最大进程数
4)/proc/sys/vm/max_map_count: Linux支持虚拟内存,也就是交换空间,可以把磁盘的一部分作为RAM的扩展,逻辑存储和物理存储的映射就要保存在地址映射表中。max_map_count限制了线程可以拥有的VMAs (Virtual Memory Areas)。
将pid_max,threads-max,max_user_process和max_map_count都改为4194000后,分别将Thread构造函数中stackSize指定为1M,512K,64K,达到的最大线程数分别是423100,626400,626400.
从中我们可以看出两点:
1)进程可用内存不仅仅局限于RAM,还包括Swap分区(虚拟内存),在线程不断创建的过程中,首先空闲的RAM不断减少,然后是空闲Swap不断减少。
2)我们在线程构造函数中指定了stackSize分别为512K和64K,但达到的最大线程数是相差无几的,说明JVM并非绝对按照我们指定的大小为线程分配内存。
综上所述,JVM最大开启线程数取决于可用内存(包括虚拟内存)和stack size, 在OS层面又和pid_max、threads-max、max_user_process和max_map_count相关。
下面是我在16G内存下,stack size 设置为512K,各项OS内核参数调整到最大,创建线程的过程中不断跟踪内存情况和轻量级线程情况的截图:
yangy@yangy:~/lizhongspace$ free -m
total used free shared buffers cached
Mem: 15935 579 15355 41 50 245
-/+ buffers/cache: 282 15652
Swap: 19070 0 19070
yangy@yangy:~/lizhongspace$ free -m
total used free shared buffers cached
Mem: 15935 4805 11130 41 50 245
-/+ buffers/cache: 4508 11426
Swap: 19070 0 19070
yangy@yangy:~/lizhongspace$ free -m
total used free shared buffers cached
Mem: 15935 7013 8921 41 50 245
-/+ buffers/cache: 6717 9217
Swap: 19070 0 19070
yangy@yangy:~/lizhongspace$ free -m
total used free shared buffers cached
Mem: 15935 8394 7540 41 50 245
-/+ buffers/cache: 8098 7836
Swap: 19070 0 19070
yangy@yangy:~/lizhongspace$ free -m
total used free shared buffers cached
Mem: 15935 11192 4742 41 50 245
-/+ buffers/cache: 10895 5039
Swap: 19070 0 19070
yangy@yangy:~/lizhongspace$ free -m
total used free shared buffers cached
Mem: 15935 12630 3304 41 50 245
-/+ buffers/cache: 12334 3600
Swap: 19070 0 19070
yangy@yangy:~/lizhongspace$ free -m
total used free shared buffers cached
Mem: 15935 14248 1686 41 50 245
-/+ buffers/cache: 13951 1983
Swap: 19070 0 19070
yangy@yangy:~/lizhongspace$ free -m
total used free shared buffers cached
Mem: 15935 15091 843 41 50 245
-/+ buffers/cache: 14794 1140
Swap: 19070 0 19070
yangy@yangy:~/lizhongspace$ free -m
total used free shared buffers cached
Mem: 15935 15758 176 27 0 32
-/+ buffers/cache: 15725 209
Swap: 19070 635 18435
yangy@yangy:~/lizhongspace$ free -m
total used free shared buffers cached
Mem: 15935 15787 147 2 0 13
-/+ buffers/cache: 15773 161
Swap: 19070 8706 10364
yangy@yangy:~/lizhongspace$ free -m
total used free shared buffers cached
Mem: 15935 15788 146 7 0 26
-/+ buffers/cache: 15761 173
Swap: 19070 10723 8347
yangy@yangy:~/lizhongspace$ ps -fL
UID PID PPID LWP C NLWP STIME TTY TIME CMD
yangy 2237 2236 2237 0 1 21:20 pts/11 00:00:00 -bash
yangy 631902 2237 631902 0 1 21:31 pts/11 00:00:00 ps -fL
以上内容同时发表在我的个人博客清欢de个人博客中,欢迎大家访问。