5、JVM可以创建多少线程

线上java程序有事会遇到下面的问题:

OutOfMemoryError: unable to create new native thread

显然,创建java线程也是受到内存限制的,下面具体分析一下原因。

操作系统内存限制

32位操作系统的最大寻址空间是2^32个字节≈4G,一个32位进程最大可使用内存一般为2G(操作系统预留2G);
64位操作系统的寻址空间大大增加,但不是简单的2^64,寻址的位数实际还受限于CPU支持的最大物理地址宽度以及操作系统具体实现的限制,64位操作系统通常使用内存分页(每页一般为4KB)管理内存,内存分页管理有很大优势:

  • 交换内存(虚拟内存) x86 默认是 4KB 分页,操作系统可以将一些长久没有使用的内存页交换到外存上(一页只有4KB,写盘是非常快的),然后这些页就可以分配到别的地方使用,这样可以使得”内存容量”大增,而调度得好的话在性能上影响不大。
  • 延迟装载 现在一个游戏安装程序动辄上G,如果一次性装载完再执行,那等个几分钟才出安装界面也是正常的,实际上各种操作系统都用CPU的分页机制实现了延迟装载:装载程序时会在进程中建立内存页到可执行文件的映射,并将相应的页表项初始化为不可用状态,等执行到这个页的指令或访问这个页的数据时CPU就会触发缺页异常,操作系统到这个时候才根据映射把相应的指令或数据读到某页内存中并修改原来不可用的页表项指向它,然后重新执行导致异常的那条指令。所以即使是上G的程序也是瞬间装载的。
  • 共享内存 我们可以复制页表来实现进程间内存的共享。比如fork子进程就很快,因为只是复制了页表,所以子进程一开始是直接使用父进程内存的。再比如动态链接库,它们的指令所占内存页是在各个进程中共享的,所以公共动态链接库的繁荣可以减少内存占用。
  • copy on write 除了上面提到的缺页异常可实现延迟装载,还有个页保护异常可实现 copy on write,每个页表项有记录该页是否可写的标志位,当某进程fork一个子进程的时候会将所有可写的内存页设置为不可写,当子进程修改数据时会触发页保护异常,操作系统会将该页复制一份让子进程修改,父进程的数据完全不受影响。

x86_64 平台上的 Linux 理论上最多支持64TB的物理内存,单个进程理论上可用128TB的地址空间(高128TB被操作系统占用了),但受限于物理内存的限制,其中最多有64TB是物理内存,其余的理论上还可以用虚拟内存(swap分区)顶包.

下图是微软官网windows系统给的内存限制:


image.png

jvm进程的内存限制

对于32位操作系统,jvm进程的最大可用内存为2G;
对于64位操作系统,jvm进程理论可以获得跟物理内存一样大的内存,但是一般linux操作系统都会对进程进行资源限制, 如使用 ulimit -S xx 命令;

jvm可创建线程数量

公式: (MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize) = 线程数

  • MaxProcessMemory 代表jvm进程最大可用的内存,32位操作系统为2G,64位操作系统取决于系统设置,如果未限制则默认等于物理内存;
  • JVMMemory 代表JVM的堆内存占用,此处也包含一些JVM堆外内存占用,如code-cachedirect-memory-bufferclass-compess-space等;
  • ReservedOsMemory 操作系统每个进程预留内存
  • ThreadStackSize 线程栈的大小,可以通过 -Xss 设置

参考资料


64位系统内存管理

linux下ulimit使用限制进程资源