栈内存溢出-StackOverflowError

Posted buguge

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了栈内存溢出-StackOverflowError相关的知识,希望对你有一定的参考价值。

下面test方法是一个死循环自己调自己的例子。运行这个方法,你会看到熟悉又不常见的java.lang.StackOverflowError(栈内存溢出错误):

 1 package com.clz;
 2 
 3 import org.junit.Test;
 4 
 5 public class TestMain {
 6     @Test
 7     public void testStackOverflowError() {
 8         testStackOverflowError();
 9     }
10 
11 }

运行结果:

java.lang.StackOverflowError
    at com.clz.TestMain.testStackOverflowError(TestMain.java:8)--(第1条stackTrace)
    at com.clz.TestMain.testStackOverflowError(TestMain.java:8)--(第2条stackTrace)
    at com.clz.TestMain.testStackOverflowError(TestMain.java:8)--(第3条stackTrace)
    。。。。。。
    at com.clz.TestMain.testStackOverflowError(TestMain.java:8)--(第500条stackTrace)
    。。。。。。
    at com.clz.TestMain.testStackOverflowError(TestMain.java:8)--(第1022条stackTrace)
    at com.clz.TestMain.testStackOverflowError(TestMain.java:8)--(第1023条stackTrace)
    at com.clz.TestMain.testStackOverflowError(TestMain.java:8)--(第1024条stackTrace)

 注意到以上运行结果,一共会打印出来1024条stacktrace信息。说明java中线程栈的长度是1024。

 

------------我是么么哒分割线----------------

 

java.lang.StackOverflowError栈内存溢出是进行复杂运算时非常容易出现的错误。

我们来看如下代码的线程栈图示

public class Demo {
    public static void main(String[] args) {
        String message="hello JVisualVM world";
        hello(message);
    }

    private static void hello(String text) {
        System.out.println(text);
    }
}

 

 

 

 

在java中,虚拟机会为每个任务的处理分配一个线程, 在这个线程里,每次调用一个方法,都会将本次方法调用的栈桢压入虚拟机栈里,这个栈桢里保存着方法内部的局部变量和其他信息。 不过呢,每个线程的虚拟机栈的大小是固定的,默认为1MB(上面的1024)。

既然一个线程的虚拟机栈内存大小是有限的,那么假设不停的调用各种方法,对应的栈桢不停的压入栈中。当这些大量的栈桢消耗完毕这个1MB的线程栈内存,最终就会导致出现栈内存溢出的情况。

 

 

------------我是么么哒分割线----------------

 

而我在上周四boss开工改版时,对redis分布式锁接口方法做了调整,却因为一个失误导致了StackOverflowError。

>>先看接口定义,然后再说问题:

 1 package com.emax.zhenghe.common.concurrent.distributeRedisLock;
 2 
 3 public interface DistributedLock {
 4         
 5      boolean lock(String key);
 6     
 7      boolean lock(String key, int retryTimes);
 8     
 9      boolean lock(String key, int retryTimes, long sleepMillis);
10     
11      boolean lock(String key, long expireMillis);
12     
13      boolean lock(String key, long expireMillis, int retryTimes);
14     
15      boolean lock(String key, long expireMillis, int retryTimes, long sleepMillis);
16     
17      boolean releaseLock(String key);
18 }

 

>>接下来说问题:

分布式锁在技术层面有两种应用场景:
1. 可以保证幂等性(防重与幂等有区别:幂等通常是对并发请求的防重控制;防重除了需要分布式保证幂等以外,还需要做数据防重校验,因为重复请求可能不是并发请求过来的,有可能是隔了很长时间的重复数据提交,就是用DCL)
2. 实现进程同步(类似于线程synchronized锁):当锁存在时,需要不断尝试重试取锁,实现自旋等待。

这个接口正好也为两种应用场景定义了方法API。问题在于,我们看这些lock重载方法,比较第7行的lock(String,int)与第11行的lock(String,long),再比较第9行的lock(String,int,long)与第13行的lock(String,long,int),太容易误用了。事实也证明了这一点,项目的业务代码里有很多用来保证幂等性的逻辑调用的是lock(String,int)或lock(String,long,int),而非lock(String,long),那么,显然无法达到幂等控制的效果。

为了解决项目中现存的这种误用,并规避日后的误用,有必要重构这个接口。如下是第一版:

 1 package com.emax.zhenghe.common.concurrent.distributeRedisLock;
 2 
 3 public interface DistributedLock {
 4     boolean lock(String key);
 5 
 6 //     boolean lock(String key, int retryTimes);
 7 
 8 //    boolean lock(String key, int retryTimes, long sleepMillis);
 9 //     boolean synchronize(String key, int retryTimes, long sleepMillis);
10 
11     boolean lock(String key, long expireMillis);
12 
13     /**
14      * 注:有很多项目很多代码调用了这个方法,过渡阶段先保留这个方法api
15      * 注:此方法不再使用。请不要使用过期的方法
16      * @param key
17      * @param expireMillis
18      * @param useless 有很多地方在用,不得不定义这个寂寞参数
19      * @return
20      */
21     @Deprecated
22     default boolean lock(String key, long expireMillis,int useless){
23         return lock(key, expireMillis, 0);
24     }
25 
26     //    boolean lock(String key, long expireMillis, int retryTimes);
27     boolean synchronize(String key, long expireMillis, int retryTimes);
28 
29     //    boolean lock(String key, long expireMillis, int retryTimes, long sleepMillis);
30     boolean synchronize(String key, long expireMillis, int retryTimes, long sleepMillis);
31 
32     boolean releaseLock(String key);
33 
34     boolean releaseLockUnsafe(String key);
35 }

 

导致出现StackOverflowError的,正是第22行标记了过时的lock(String key, long expireMillis,int useless)。这个方法调用的是其自身!而我的本意是要它调用第11行的lock(key, expireMillis):

。。。
13     /**
14      * 注:有很多项目很多代码调用了这个方法,过渡阶段先保留这个方法api
15      * 注:此方法不再使用。请不要使用过期的方法
16      * @param key
17      * @param expireMillis
18      * @param useless 有很多地方在用,不得不定义这个寂寞参数
19      * @return
20      */
21     @Deprecated
22     default boolean lock(String key, long expireMillis,int useless){
23         return lock(key, expireMillis);
24     }
。。。

 

以上是关于栈内存溢出-StackOverflowError的主要内容,如果未能解决你的问题,请参考以下文章

JPA出现recursion 死循环导致栈内存溢出问题 Could not write JSON: Infinite recursion (StackOverflowError)

五种内存溢出案例总结:涵盖栈深度溢出永久代内存溢出本地方法栈溢出JVM栈内存溢出和堆溢出

五种内存溢出案例总结:涵盖栈深度溢出永久代内存溢出本地方法栈溢出JVM栈内存溢出和堆溢出

栈溢出解决

Java栈内存堆内存溢出及相关JVM参数配置总结

使用lombok的@Data会导致栈溢出StackOverflowError