JVM栈
根据JVM规范,JVM包括两种栈,java虚拟机栈和本地方法栈。也就是说,每当启动一个java线程时,JVM就会为其分配一个java虚拟机栈和一个本地方法栈。栈对线程而言是独立私有内存区域,线程间无法对栈进行互访,这由虚拟机保证。如下图:
java虚拟机栈在老的规范中描述为java栈,顾名思义,是为java方法即java 字节码方法调用服务的栈。如同C栈,每当java线程调用java方法时,JVM会向java虚拟机栈压入新的栈帧,该栈帧成为当前帧,用来存储局部变量,中间运算值等数据。
本地方法栈是为本地方法调用服务的栈,如通过JNI、JNA接口实现的本地方法调用,方法栈具体是什么类型由本地方法接口的实现决定,如其采用C链接模型,那么本地方法栈就是C栈。JVM规范并没有对本地方法栈使用的语言、数据结构进行强制规定,由具体虚拟机实现决定。当线程调用本地方法时,java虚拟机栈并不会改变,JVM只是动态链接和调用本地方法,由本地方法栈完成本地方法的压栈出栈。
JVM栈对线程并发量的影响
由上可知,JVM会为每个线程分配虚拟机栈和本地方法栈,但受资源的约束线程数是无法无限增长的,这除了操作系统设置的影响外,JVM栈大小对JVM能创建的线程数也有间接影响。
操作系统为每个进程分配的内存是有限的,在粗略计算下,抛开其它小的消耗,分配给JVM进程的内存减去JVM堆、方法区、基本就是栈区所能使用的最大内存了。如win32下,一个JVM最大内存是2G,如果分配给堆和方法区1G,剩下1G是栈区所能使用的内存,每创建一个线程就需要分配一定的栈内存给线程,线程数必然可尽。
创建新线程无法申请到足够栈内存,容易出现内存溢出错误,或者JVM崩溃。这常出现在小内存,大并发线程量的应用中。如下的虚拟机崩溃问题也许似曾相识:
#
# There is insufficient memory for the Java Runtime Environment to continue.
# Native memory allocation (mmap) failed to map 133419008 bytes for committing reserved memory.
# Possible reasons:
# The system is out of physical RAM or swap space
# In 32 bit mode, the process size limit was hit
# Possible solutions:
# Reduce memory load on the system
# Increase physical memory or swap space
# Check if swap backing store is full
# Use 64 bit Java on a 64 bit OS
# Decrease Java heap size (-Xmx/-Xms)
# Decrease number of Java threads
# Decrease Java thread stack sizes (-Xss)
# Set larger code cache with -XX:ReservedCodeCacheSize=
# This output file may be truncated or incomplete.
#
这样的应用很常见,尤其是把服务架设在云服务器上用几十块钱一个月的机器当服务器的初创团队。
除了可以在服务器资源投入上解决问题外,依然可以在现有的有限条件下解决问题:一是降低其他内存区的内存占用量,如堆区,二是采用workaround,任务调度方案,减少并发执行量,另一个很少受到关注的方法是调整java线程栈的大小。
-Xss可用来设置JVM单个线程的栈大小,该参数不带单位时指字节,后面可以带k或K,m或M,g或G等单位,分别表示KB、MB、GB。JDK5以后每个java线程的栈内存默认是1024KB,即1M,1G的JVM栈最多支持1024个线程。如果应用中并无很深的调用,根据应用的特点降低JVM栈大小也是能达到较大的线程并发量,如-Xss256m。这样1G的栈内存可支持4*1024个线程。另外,需要补充说明的是,Hotspot做为主流JVM,其实现是将java虚拟机栈和本地方法栈合二为一,因此,如Xoss针对本地方法区大小设置是无效的。
参考
深入理解Java虚拟机 周志明
深入Java虚拟机 Bill Venners
Java虚拟机规范8 https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.6