简体   繁体   English

为什么这段代码在java中没有内存,而在c中却没有?

[英]Why does this code run out of memory in java, but not in c?

In either java or c I can write a function like 在java或c中,我可以编写一个类似的函数

fun(){
  fun();
}

(ignoring syntax details) (忽略语法细节)

In java I get OutOfMemory exception but in C (and maybe some other languages) it seems to run forever, as if it were an infinite loop. 在java中,我得到了OutOfMemory异常,但是在C(也许还有其他一些语言)中它似乎永远运行,好像它是一个无限循环。 Why don't I get OutOfMemory error here as well? 为什么我不在这里得到OutOfMemory错误?

由于你的函数是尾递归的一个例子,所以很可能,C编译器正在优化迭代的递归,导致它无限循环而不会崩溃。

Other answerers are correct that there's some compiler magic that converts the tail recursion to iteration, though it depends on the optimization settings of the compiler. 其他的回答是正确的,有一些编译器魔法将尾递归转换为迭代,尽管它取决于编译器的优化设置。 In gcc for instance, if we compile with gcc -S -O1 someFile.c (given your code), we get the following generated assembly: 例如,在gcc中,如果我们用gcc -S -O1 someFile.c编译(给定你的代码),我们得到以下生成的程序集:

fun:
.LFB2:
        pushq   %rbp
.LCFI0:
        movq    %rsp, %rbp
.LCFI1:
        movl    $0, %eax
        call    fun
        leave
        ret
.LFE2:
        .size   fun, .-fun

So you can see, it's still using call/leave/ret instructions to perform an actual function call, which will kill the process. 所以你可以看到,它仍然使用call / leave / ret指令来执行实际的函数调用,这会终止进程。 Once you start optimizing further with gcc -S -O2 someFile.c we start getting the magic: 一旦你开始使用gcc -S -O2 someFile.c进一步优化,我们开始获得魔力:

fun:
.LFB24:
        .p2align 4,,10
        .p2align 3
.L2:
        jmp     .L2
.LFE24:    
        .size   fun, .-fun
        .p2align 4,,15

It depends on your compiler and your compiler settings, so it helps to be friends with them. 这取决于您的编译器和编译器设置,因此有助于成为他们的朋友。

The reason why is that the C compiler is likely treating this as a tail recusive call and hence avoiding building a stack for the execution of the function. 原因是C编译器可能将其视为尾部重用调用,从而避免构建用于执行函数的堆栈。 Since no stack is built up for the call it turns from recursion into a simple infinite loop execution. 由于没有为调用构建堆栈,因此它从递归变为简单的无限循环执行。 You can force it to build up a stack by making it head recursive 您可以强制它通过使其头部递归来构建堆栈

int fun() { 1 + fun(); }

As others have pointed out this is a tail call recursion optimization done by the C compiler. 正如其他人已经指出的那样,这是由C编译器完成的尾调用递归优化。 As always, it helps to look at a concrete example. 与往常一样,有助于查看具体示例。 Without any optimizations enabled gcc (v3.4.6) produces the following x86 assembly code:- 如果没有启用任何优化,gcc(v3.4.6)将生成以下x86汇编代码: -

fun:
    pushl   %ebp
    movl    %esp, %ebp
    call    fun
    leave
    ret
    .size   fun, .-fun

Notice the recursive call to fun(). 注意对fun()的递归调用。 If this executes it will eventually overflow its stack and crash, but at -O2 gcc produces:- 如果执行它最终将溢出其堆栈并崩溃,但在-O2 gcc产生: -

    fun:
        pushl   %ebp
        movl    %esp, %ebp
.L2:
        jmp     .L2
        .size   fun, .-fun

Notice the endless loop without a return instruction? 注意没有返回指令的无限循环? This will simply execute forever. 这将永远执行。

In C, this could be optimized as a tail-recursive call. 在C中,这可以作为尾递归调用进行优化。 So the call to fun() doesn't really call itself; 所以对fun()的调用并没有真正调用自己; it just restarts the function (like a goto). 它只是重新启动该功能(如goto)。 In other words, the compiler treats it as if it had been written like this: 换句话说,编译器将其视为如下所示:

void fun() {
start:
    goto start;
}

So, the stack will not grow. 所以,堆栈不会增长。

If a programming language's implementation has tail call optimization , then it will compile that recursion into a loop. 如果编程语言的实现具有尾调用优化 ,那么它会将该递归编译成循环。 The current Java VM does not have tail call optimization, so it will end up in java.lang.StackOverflowError. 当前的Java VM没有尾调用优化,因此最终会出现在java.lang.StackOverflowError中。

Probably some time in the future the Java VM will have tail call optimization, because the functional programming languages which run on JVM (Scala, Clojure etc.) would benefit from it (right now at least the Scala compiler does its own tail call optimization for direct recursion , but AFAIK not for indirect recursion). 可能在未来的某个时候Java VM将具有尾调用优化,因为在JVM(Scala,Clojure等)上运行的函数式编程语言将从中受益(现在至少Scala编译器自己进行尾调用优化) 直接递归 ,但AFAIK不用于间接递归)。

Your c compiler is probably using using tail recursion. 你的c编译器可能正在使用尾递归。 Every time you enter into a new function the computer adds an entry to the stack. 每次进入新功能时,计算机都会向堆栈添加一个条目。 This entry states where the CPU should jump back to after the routine is done. 此条目指出CPU应在例程完成后跳回到的位置。 Now in the case given above, since the call to fun() inside fun() is the last call in the function the c compiler may be optimzing out the stack push and instead and creating a tailcall. 现在在上面给出的情况下,由于fun()中fun()的调用是函数中的最后一次调用,因此c编译器可能会优化堆栈推送,而是创建一个尾调用。 You can actually use this to create a loop: 您实际上可以使用它来创建循环:

int foo(int from, int to)
{
    if (from == to) return from;
    dosomething();
    from ++;
    foo(from, to);
}

Many languages (for example Erlang) don't have loops at all and instead use the above method to create loops. 许多语言(例如Erlang)根本没有循环,而是使用上面的方法来创建循环。

Java does not support tail recursion. Java不支持尾递归。

You will get an abnormal program termination after some time. 一段时间后,您将收到异常程序终止。 Your code contains an indefinitive recursion and each call to fun() put sadditional bytes on the stack. 您的代码包含一个不确定的递归,每次调用fun()都会在堆栈中放置一些条件。 Depending on the size of your memory and your limits, the application will terminate after at least some 500 mil calls. 根据您的内存大小和限制,应用程序将在至少约500密耳的呼叫后终止。 This might take some time, but you will get an exceptional termination. 这可能需要一些时间,但您将获得特殊终止。

The java VM limits the depth of recursion to some level, therefore it will terminate soon. java VM将递归深度限制在某个级别,因此它很快就会终止。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM