本节笔者分享一个在实际工作中遇到的​​栈内存溢出​​(StackOverflowError)问题,以及其解决方案。

问题介绍:笔者负责的一个Java Web项目在启动的时候,需要有一些初始化操作,而接下来的代码的执行必须要等到相关初始化操作完成。为了实现这个等待的功能,这个项目之前的负责人使用了一个​​递归方法​​进行判断,最终导致了应用每次在启动的时候,都会出现StackOverFlowException异常。

首先我们介绍,为什么递归操作会引起栈内存溢出?

线程在执行的时候,会直接调用一系列的方法,而被直接调用的方法可能又调用了其他的方法。为了保证直接或者间接被调用的方法可以按照方法声明的顺序那样进行执行,每个线程都在栈内存中维护了一个数据结构,就是一个栈。一个线程在执行的时候,每当遇到一个方法的开始,就将这个方法的相关信息(称之为​​栈帧​​)压入栈,而方法结束后再进行弹栈。通过这种方式保证了方法执行顺序的正确性。

但是因为栈内存的大小是有限制的,默认请下一般是1M。当我们再使用递归方法的时候,方法会存在不断的循环调用,因此会不断的往栈中压入数据,当数据量超过1M的时候,就会出现栈内存溢出。

为了说明上面这个问题,请看以下的代码演示:

1. 

2.
public class StackOverFlowDemo {

3.
int count=0;

4.
public void recursiveMethod(){

5.
if (count==1000000){//递归方法执行1000000次时,结束

6.
return;

7.
}

8.
count++;

9.
System.out.println("执行了:"+count+"次");

10.
recursiveMethod();//递归调用

11.
}

12.
public static void main(String[] args) {

13.
StackOverFlowDemo stackOverFlowDemo = new StackOverFlowDemo();

14.
stackOverFlowDemo.recursiveMethod();

15.
System.out.println("执行其他代码...");

16.
}

17. }
18.


在这段代码中,我们定义了一个递归方法​​recursiveMethod()​​,我们希望递归方法执行了1百万次之后结束,为了便于观察,每次递归都打印出当前是第几次迭代。但是真的可以递归1百万次吗?以下是其中一次运行结果:


执行了:3978次

执行了:3979次

执行了:3980次

执行了:3981次

Exception in thread "main" java.lang.StackOverflowError

at sun.nio.cs.UTF_8.updatePositions(UTF_8.java:77)

at sun.nio.cs.UTF_8.access$200(UTF_8.java:57)

at sun.nio.cs.UTF_8$Encoder.encodeArrayLoop(UTF_8.java:636)

at sun.nio.cs.UTF_8$Encoder.encodeLoop(UTF_8.java:691)

at java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:579)

at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:271)

at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:125)


可以看到,事实上递归了3981次的后,就抛出了StackOverflowError异常。这提示我们,在Java开发过程中,要慎用递归,尤其是在不能预估递归方法大概需要执行多少次的时候,最好就不好使用。

为了解决上述问题,我们需要进行一些改造。

1. 

2.
public class SpinLockDemo {

3.
int count=0;

4.
public void incr(){

5.
count++;

6.
System.out.println("执行了:"+count+"次");

7.
8.
}

9.
public static void main(String[] args) {

10.
SpinLockDemo spinLockDemo = new SpinLockDemo();

11.
while(spinLockDemo.count!=1000000){//这段代码其实就是一个自旋锁

12.
spinLockDemo.incr();

13.
}

14.
System.out.println("执行其他代码...");

15.
}

16. }
17.


因为我们只是希望count变量值达到1百万的时候,才继续执行剩余部分的代码。所以我们可以将判断条件放入一个while循环中,只要没到1000000次,就继续增加。到达之后,循环结束,执行剩余部分的代码。这里的while循环,其实就是所谓的​​自旋锁​​(Spin Lock)。需要注意的是:自旋锁不是真正的锁,其只是解决思路的一种方式,只要不能继续往下执行,就不断的循环。