学习内容:第2章 - Java 内存区域与内存溢出异常
实战代码均在我的 Git 仓库中:https://github.com/nx-xn2002/JVM-Learn.git
虚拟机栈和本地方法栈溢出
HotSpot 虚拟机中不区分虚拟机栈和本地方法栈,因此对于 HotSpot 来说,设置本地方法栈大小的参数 -Xoss
虽然存在,实际上却没有任何效果,栈容量只能使用 -Xss
参数来设定
对于虚拟机栈和本地方法栈,Java 虚拟机规范中描述了两种异常:
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出
StackOverflowError
异常 - 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出
OutOfMemoryError
异常
Java 虚拟机可以实现自行选择是否支持栈的动态扩展,而 HotSpot 虚拟机的选择是不支持扩展,因此只有在创建线程申请内存时就因为无法获取到足够内存而出现 OutOfMemoryError
异常,否则在线程运行时是不会因为扩展而导致内存溢出的,只会因为栈容量无法容纳新的栈帧而导致 StackOverflowError
异常
在此,作者设计了两个实验来进行验证在单线程中操作,这些行为是否能够让 HotSpot 虚拟机产生OutOfMemoryError
异常:
- 实验一:使用
-Xss
参数减少栈内存容量
预计结果:抛出StackOverflowError
异常,异常出现时,输出的堆栈深度相应缩小 - 实验二:定义大量的本地变量,增大当前方法帧中本地变量表的长度
预计结果:抛出StackOverflowError
异常,异常出现时,输出的堆栈深度相应缩小
实验一:使用 -Xss
参数减少栈内存容量
按照书中内容,我使用了以下代码进行运行:
/**
* Java 虚拟机栈 StackOverflowError实验一:使用 -Xss 参数减少栈内存容量
* VM Options:-Xss128k
*
* @author Ni Xiang
*/
public class JavaVMStackSOF1 {
private int stackLength = 0;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable {
JavaVMStackSOF1 oom = new JavaVMStackSOF1();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}
将 VM Options 设置为 -Xss128k
后,运行得到以下结果,抛出 StackOverflowError
异常:
而当设置为 256k 后,可见栈深度增大了:
注意事项
作者在书中指出,对于不同版本的 Java 虚拟机和不同的操作系统,栈容量最小值可能会有所限制。对此,我进行了尝试:
我使用的是 64 位 Windows 系统下的 JDK 17,由此可见,我的栈容量最小值是 84k
实验二:定义大量的本地变量,增大当前方法帧中本地变量表的长度
验证第二种情况,使用如下代码,定义大量变量来多占局部变量表空间
/**
* Java 虚拟机栈 StackOverflowError实验二:定义大量的本地变量,增大当前方法帧中本地变量表的长度
*
* @author Ni Xiang
*/
public class JavaVMStackSOF2 {
private static int stackLength = 0;
public static void stackLeak() {
long unused1, unused2, unused3, unused4, unused5, unused6, unused7, unused8, unused9, unused10, unused11,
unused12, unused13, unused14, unused15, unused16, unused17, unused18, unused19, unused20, unused21,
unused22, unused23, unused24, unused25, unused26, unused27, unused28, unused29, unused30, unused31,
unused32, unused33, unused34, unused35, unused36, unused37, unused38, unused39, unused40, unused41,
unused42, unused43, unused44, unused45, unused46, unused47, unused48, unused49, unused50, unused51,
unused52, unused53, unused54, unused55, unused56, unused57, unused58, unused59, unused60, unused61,
unused62, unused63, unused64, unused65, unused66, unused67, unused68, unused69, unused70, unused71,
unused72, unused73, unused74, unused75, unused76, unused77, unused78, unused79, unused80, unused81,
unused82, unused83, unused84, unused85, unused86, unused87, unused88, unused89, unused90, unused91,
unused92, unused93, unused94, unused95, unused96, unused97, unused98, unused99, unused100;
stackLength++;
stackLeak();
unused1= unused2= unused3= unused4= unused5= unused6= unused7= unused8= unused9= unused10= unused11=
unused12= unused13= unused14= unused15= unused16= unused17= unused18= unused19= unused20= unused21=
unused22= unused23= unused24= unused25= unused26= unused27= unused28= unused29= unused30= unused31=
unused32= unused33= unused34= unused35= unused36= unused37= unused38= unused39= unused40= unused41=
unused42= unused43= unused44= unused45= unused46= unused47= unused48= unused49= unused50= unused51=
unused52= unused53= unused54= unused55= unused56= unused57= unused58= unused59= unused60= unused61=
unused62= unused63= unused64= unused65= unused66= unused67= unused68= unused69= unused70= unused71=
unused72= unused73= unused74= unused75= unused76= unused77= unused78= unused79= unused80= unused81=
unused82= unused83= unused84= unused85= unused86= unused87= unused88= unused89= unused90= unused91=
unused92= unused93= unused94= unused95= unused96= unused97= unused98= unused99= unused100=0;
}
public static void main(String[] args) throws Throwable {
try {
stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + stackLength);
throw e;
}
}
}
结果如下,依然是抛出 StackOverflowError
异常:
结论
上述两个实验表明,无论是由于栈帧太大还是虚拟机容量太小,当新的栈帧内存无法分配的时候,HotSpot 虚拟机抛出的都是 StackOverflowError
异常
HotSpot 创建线程导致内存溢出异常
如果测试的时候不限于单线程,通过不断创建线程的方法,在 HotSpot 上也可以产生内存溢出异常。但这样产生的内存溢出异常和占空间是否足够并不存在任何直接的关系,主要取决于操作系统本身的内存使用状态。在这种情况下,给每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。
实践代码如下:
/**
* Java 虚拟机栈 OutOfMemory - 创建线程导致的内存溢出异常
* VM Options:-Xss2M
*
* @author Ni Xiang
*/
public class JavaVMStackOOM {
private void dontStop() {
while (true) {
}
}
public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) throws Throwable{
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}
将虚拟机栈内存的大小设置为了 2M,运行后出现了作者预警的严重系统卡顿的情况,但在此之后,并没有出现预期的 OOM 抛出。尝试几次后,也尝试过将内存大小增大至 512M,依然无效。
暂未找到原因,看作者文内一直说的是在 32 位的 Windows 上,可能是系统的差异?或者可能只能归结于概率事件吧,有知道的老哥可以在评论区说一下