24

JVM 源码分析之一个 Java 进程究竟能创建多少线程 | PerfMa应用性能技术社区

 4 years ago
source link: https://club.perfma.com/article/244079?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
你假笨11月前

虽然这篇文章的标题打着JVM源码分析的旗号,不过本文不仅仅从 JVM 源码角度来分析,更多的来自于 Linux Kernel 的源码分析,今天要说的是 JVM 里比较常见的一个问题。

这个问题可能有几种表述

  • 一个Java进程到底能创建多少线程?
  • 到底有哪些因素决定了能创建多少线程?
  • java.lang.OutOfMemoryError: unable to create new native thread的异常究竟是怎么回事

不过我这里先声明下可能不能完全百分百将各种因素都理出来,因为毕竟我不是做 Linux Kernel 开发的,还有不少细节没有注意到的,我将我能分析到的因素和大家分享一下,如果大家在平时工作中还碰到别的因素,欢迎在文章下面留言,让更多人参与进来讨论

从 JVM 说起

线程大家都熟悉,new Thread().start()即会创建一个线程,这里我首先指出一点new Thread()其实并不会创建一个真正的线程,只有在调用了 start 方法之后才会创建一个线程,这个大家分析下 Java 代码就知道了,Thread 的构造函数是纯 Java 代码,start 方法会调到一个 native 方法 start0 里,而 start0 其实就是JVM_StartThread这个方法。

1.jpg

从上面代码里首先要大家关注下最后的那个 if 判断 if (native_thread->osthread() == NULL),如果 osthread 为空,那将会抛出大家比较熟悉的 unable to create new native thread OOM 异常,因此 osthread 为空非常关键,后面会看到什么情况下osthread会为空。

另外大家应该注意到了native_thread = new JavaThread(&thread_entry, sz),在这里才会真正创建一个线程。

2.jpg

上面代码里的os::create_thread(this, thr_type, stack_sz)会通过pthread_create来创建线程,而 Linux 下对应的实现如下:

3.jpg
4.jpg
5.jpg

如果在 new OSThread 的过程中就失败了,那显然 osthread 为 NULL,那再回到上面第一段代码,此时会抛出java.lang.OutOfMemoryError: unable to create new native thread的异常,而什么情况下new OSThread会失败,比如说内存不够了,而这里的内存其实是 C Heap,而非 Java Heap,由此可见从 JVM 的角度来说,影响线程创建的因素包括了 Xmx,MaxPermSize,MaxDirectMemorySize,ReservedCodeCacheSize 等,因为这些参数会影响剩余的内存

另外注意到如果pthread_create执行失败,那通过thread->set_osthread(NULL)会设置空值,这个时候 osthread 也为 NULL,因此也会抛出上面的 OOM 异常,导致创建线程失败,因此接下来要分析下 pthread_create 失败的因素。

glibc 中的 pthread_create

stack_size

pthread_create 的实现在 glibc 里,

6.jpg

上面我主要想说的一段代码是int err = ALLOCATE_STACK (iattr, &pd),顾名思义就是分配线程栈,简单来说就是根据 iattr 里指定的 stackSize,通过 mmap 分配一块内存出来给线程作为栈使。

那我们来说说 stackSize,这个大家应该都明白,线程要执行,要有一些栈空间,试想一下,如果分配栈的时候内存不够了,是不是创建肯定失败?而 stackSize 在 JVM 下是可以通过 -Xss 指定的,当然如果没有指定也有默认的值,下面是 JDK6 之后(含)默认值的情况。

Linux Kernel 里的 clone

如果栈分配成功,那接下来就要创建线程了,大概逻辑如下
7.jpg
而create_thread其实是调用的系统调用clone

8.jpg

系统调用这块就切入到了 Linux Kernel 里

clone 系统调用最终会调用do_fork方法,接下来通过剖解这个方法来分析 Kernel 里还存在哪些因素。

max_user_processes

9.jpg

先看这么一段,这里其实就是判断用户的进程数有多少,大家知道在linux下,进程和线程其数据结构都是一样的,因此这里说的进程数可以理解为轻量级线程数,而这个最大值是可以通过ulimit -u可以查到的,所以如果当前用户起的线程数超过了这个限制,那肯定是不会创建线程成功的,可以通过ulimit -u value来修改这个值

max_map_count

在这个过程中不乏有 mallo c的操作,底层是通过系统调用 brk 来实现的,或者上面提到的栈是通过 mmap 来分配的,不管是 malloc 还是 mmap,在底层都会有类似的判断。
10.jpg
如果进程被分配的内存段超过sysctl_max_map_count就会失败,而这个值在 linux 下对应/proc/sys/vm/max_map_count,默认值是 65530,可以通过修改上面的文件来改变这个阈值

max_threads

还存在max_threads的限制,代码如下:

11.jpg

如果要修改或者查看可以通过/proc/sys/kernel/threads-max来操作, 这个值是受到物理内存的限制,在fork_init的时候就计算好了。

12.jpg

pid_max

pid 也存在限制

13.jpg

alloc_pid的定义如下

14.jpg

alloc_pidmap中会判断pid_max,而这个值的定义如下

15.jpg

这个值可以通过 /proc/sys/kernel/pid_max 来查看或者修改

通过对 JVM,glibc,Linux kernel 的源码分析,我们暂时得出了一些影响线程创建的因素,包括

  • JVM:Xmx,Xss,MaxPermSize,MaxDirectMemorySize,ReservedCodeCacheSize 等
  • Kernel:max_user_processes,max_map_count,max_threads,pid_max 等

由于对 kernel 的源码研读时间有限,不一定总结完整,大家可以补充。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK