浅析StackOverFlow

StackOverFlow 在 Java 里是一个Error,当发生 StackOverFlow 错误时说明系统出现了严重问题。Java 应用中出现 Error 时,应用会直接退出(不能 catch Error)。

Java程序员日常开发中很少遇到该类错误。 StackOverflowError.java 源代码中明确说明:由于应用的递归调用太深导致栈溢出,从而发生该错误。而Java程序员一般在开发中很少使用递归(JavaScript等函数式语言程序员或更注重代码设计结构高级程序员可能会更喜欢使用递归),一般使用递归的场景用循环也能解决(实际上使用循环也是解决 StackOverflowError 的一种方法)。

我们先回忆下什么是是Java虚拟机中的一种内存结构,用来存放一组栈帧,随着线程创建而创建,随着线程的中止而被回收,为创建线程所私有。栈帧则是在方法调用时创建,方法执行完毕或异常终止时销毁 栈帧栈帧存放本地变量,中间计算结果等。

我们可以使用-Xss设置了JVM的栈大小(stack size),JDK5以后每个JVM栈大小为1M,以前每个JVM栈大小为256K。当一个线程创建时,创建;当线程中有方法调用时,创建 栈帧 并压入栈;当在方法调用内部有调用其他方法时,创建新的栈帧并压入栈。如果有递归方法调用,每递归调用一次就创建一个栈帧并压入,当的大小超过 -Xss设置的大小时,就会抛出 StackOverFlow 错误。

当然我们可以使用可扩展的,这样就不会出现 StackOverFlow 错误,而是可能出现 OutOfMemory错误。

接下来,我们会分析Java中怎样出现 StackOverFlow 以及避免出现StackOverFlow;还会分析Scala中的尾递归为什么不会出现StackOverFlow,而Scala中的首递归同样会出现StackOverFlow的原因。show me the code:

Java 递归出现 StackOverFlow Error

public class Rec1 {
    public static void main(String[] args) {
        process(0);
    }

    private static void process(int n) {
        System.out.println(n);
        process(n + 1);
    }
}

n大概加到7000左右时会出现 StackOverFlow Error,并退出应用。

分析反编译后的代码:

  private static void process(int);
    descriptor: (I)V
    flags: ACC_PRIVATE, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: iload_0
         4: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V

         7: iload_0
         8: iconst_1
         9: iadd

        10: invokestatic  #2                  // Method process:(I)V
        13: return

从0~4行完成打印操作后,7~8行实现本地变量0(静态方法的本地变量0即是参数1)与常量1相加,结果替换本地变量0;第10行调用其他方法(递归调用本方法),这个地方会创建栈帧。当递归调用一直往下调用(递归调用一般要设置退出递归的条件,防止无限递归),就会出现栈溢出错误。

Java 循环解决 StackOverFlow Error

public class Rec2 {
    public static void main(String[] args) {
        process(0);
    }

    private static void process(int n) {
        for(;;) {
            System.out.println(n);
            n += 1;
        }
    }
}

分析反编译后的代码:

  private static void process(int);
    descriptor: (I)V
    flags: ACC_PRIVATE, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: iload_0
         4: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
         7: iinc          0, 1
        10: goto          0

0~4行依然是打印操作;第7行实现本地变量0自增1(安全操作,与i++不同),第10行的地方我们看到使用的是 goto指令,并不是invoke*指令。这个地方没有方法调用,不会创建栈帧,会使用当前栈帧继续执行指令。

Scala 中的尾递归没有 StackOverFlow Error

object Hello {
    def main(args: Array[String]): Unit = {
        process(1);
    }

    def process(line: Integer): Integer = {
        println(line);
        if(line > 100000) {
          return line;
        }
        process(line + 1);

    }
}

以下是反编译后的代码:

Hello.class
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #16                 // Field Hello$.MODULE$:LHello$;
         3: aload_0
         4: invokevirtual #22                 // Method Hello$.main:([Ljava/lang/String;)V
         7: return

Hello$.class
  public void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=2, args_size=2
         0: aload_0
         1: getstatic     #19                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
         4: iconst_1
         5: invokevirtual #23                 // Method scala/Predef$.int2Integer:(I)Ljava/lang/Integer;
         8: invokevirtual #27                 // Method process:(Ljava/lang/Integer;)Ljava/lang/Integer;
        11: pop
        12: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      13     0  this   LHello$;
            0      13     1  args   [Ljava/lang/String;
      LineNumberTable:
        line 3: 0

  public java.lang.Integer process(java.lang.Integer);
    descriptor: (Ljava/lang/Integer;)Ljava/lang/Integer;
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=2, args_size=2
         0: getstatic     #19                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
         3: aload_1
         4: invokevirtual #34                 // Method scala/Predef$.println:(Ljava/lang/Object;)V

         7: getstatic     #19                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
        10: aload_1
        11: invokevirtual #38                 // Method scala/Predef$.Integer2int:(Ljava/lang/Integer;)I
        14: ldc           #39                 // int 100000
        16: if_icmple     21

        19: aload_1
        20: areturn

        21: getstatic     #19                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
        24: getstatic     #19                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
        27: aload_1
        28: invokevirtual #38                 // Method scala/Predef$.Integer2int:(Ljava/lang/Integer;)I
        31: iconst_1
        32: iadd
        33: invokevirtual #23                 // Method scala/Predef$.int2Integer:(I)Ljava/lang/Integer;
        36: astore_1

        37: goto          0

分析反编译后的代码我们可以发现,Hello.scala编译后生成了两个class文件(Hello.class, Hello$.class)。

Hello.class中main方法是入口,持有一个Hello$类型的对象MODULE$。main方法调用MODULE$的process方法。

而Hello$.class中的代码基本反映了Hello.scala源代码。main方法反映了Hello.scala中的main方法(只是此处不是静态方法)。首先会调用Predef$.println打印本地变量1(也就是方法参数1,实例方法的参数0默认是this);接着if_icmple判断条件;然后把本地变量1和常量1相加,结果替换本地变量1;最后37行的goto指令跳转到0行继续执行(没有调用方法,所以不会创建栈帧,和上面的for循环一样)。

Scala 中的非尾递归也会有 StackOverFlow Error

object Hello1 {
    def main(args: Array[String]): Unit = {
        process(1);
    }

    def process(line: Integer): Integer = {
        println(line);
        if(line > 100000) {
          return line;
        }
        process(line + 1) + 1;

    }
}

默认情况下line加到3000多时出现栈溢出错误。

分析反编译代码:

  public java.lang.Integer process(java.lang.Integer);
    descriptor: (Ljava/lang/Integer;)Ljava/lang/Integer;
    flags: ACC_PUBLIC
    Code:
      stack=6, locals=2, args_size=2
         0: getstatic     #19                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
         3: aload_1
         4: invokevirtual #34                 // Method scala/Predef$.println:(Ljava/lang/Object;)V

         7: getstatic     #19                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
        10: aload_1
        11: invokevirtual #38                 // Method scala/Predef$.Integer2int:(Ljava/lang/Integer;)I
        14: ldc           #39                 // int 100000
        16: if_icmple     21

        19: aload_1
        20: areturn

        21: getstatic     #19                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
        24: getstatic     #19                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
        27: aload_0
        28: getstatic     #19                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
        31: getstatic     #19                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
        34: aload_1
        35: invokevirtual #38                 // Method scala/Predef$.Integer2int:(Ljava/lang/Integer;)I
        38: iconst_1
        39: iadd
        40: invokevirtual #23                 // Method scala/Predef$.int2Integer:(I)Ljava/lang/Integer;
        43: invokevirtual #27                 // Method process:(Ljava/lang/Integer;)Ljava/lang/Integer;

        46: invokevirtual #38                 // Method scala/Predef$.Integer2int:(Ljava/lang/Integer;)I
        49: iconst_1
        50: iadd
        51: invokevirtual #23                 // Method scala/Predef$.int2Integer:(I)Ljava/lang/Integer;
        54: areturn

我们主要关注43行的process递归调用,这里是发生栈溢出的关键。

results matching ""

    No results matching ""