Why JavaScript loops do not overflow stack?

Viewed 112

I've got a couple of questions about memory allocation in JavaScript

As far as I know, JavaScript primitives are immutable and stored in the stack. If we change the value of a primitive or if we assign a new variable to the old variable, it creates a new memory location for each case.

let x = 2 
let y = x
x = 3
console.log(y) // 2

  1. When we run a large loop just as the example bellow, 99999999 times 8bytes needed in stack to allocate space for i. So, why the stack is not overflown?
    for(let i = 0; i<99999999; i++){
       let x = i
    }
  1. If millions of objects are created simultaneously (in a real world app), is the stack enough to hold references to all the objects in the heap?
3 Answers

(V8 developer here.)

  1. When we run a large loop just as the example bellow, 99999999 times 8bytes needed in stack to allocate space for i. So, why the stack is not overflown?

The premise is incorrect. There's only one stack slot for i. Each iteration of the loop reuses it. Whether this loop runs once, or twice, or 100 times, or 9999... times therefore doesn't change how much stack space it needs.

  1. If millions of objects are created simultaneously (in a real world app), is the stack enough to hold references to all the objects in the heap?

The usable stack size is a little less than a megabyte (this is determined by the operating system); on a 64-bit platform that's enough for around 100K pointers, which (very roughly) translates to several tens of thousand local variables distributed across all functions that currently have an activation (there's no specific number because "it depends"). So it's certainly possible to have more objects on the heap than can be referred to directly from the stack, but this is not typically a limitation that code runs into: for example, you could have a local variable pointing at an array, and that array can in turn refer to thousands of other objects. That way, a single stack slot can keep many objects easily accessible.

(Nitpick: millions of objects are never created "simultaneously", they're always allocated after each other, maybe in quick succession.)

the key point here is that the code includes a garbage collector, once a memory allocation has been narked as not required, the GC is allowed to reallocated for a different use

this is a little oversimplified but lets looks at what's happening at each stage

let x = 3;
let y = x; 
x = 2; 

**sudo**
allocate x;
set x = 3;
allocate y;
set y = read x
set x = 2

so the stack never exceeds a single state that is scoped to 2 allocuted variables

for your loop example

for(let i = 0; i<99999999; i++){ //
   let x = i
}

**sudo**
label forloop;
allocate i;
set i = 0;

allocate x;
set x = read i;

if i < 99999999 
    set i = read i + 1
    goto forloop
else
    deallocate x;
    deallocate i;

here their may be some growth in the memory usage from the allocation of the x variable in the loop however GC can easily clean this up

now for the issue you haven't considered functions and recursion

function doSomething(i){
    if(i>99999999){
        return i;
    else
        return doSomething(i+1);
}
doSomething(0)

**sudo**
goto main;
label doSomething;

if read current_state_i  < 99999999
    allocate new_state;
    allocate i in new_state;
    set new_state_i = read current_state_i + 1
    stack_push doSomething using new_state
    //stack_pop returns here
    set current_state_return = read new_state_return
else
    set current_state_return = current_state_i
    
deallocate new_state
goto stack_pop

label main;
allocate dosomething_state;
allocate i in dosomething_state;
set dosomething_state_i = 0;
stack_push doSomething using dosomething_state
//stack_pop returns here

for this one every time you call the function doSomething the stack has an state added to it this state is a memory slot that stores the passed in value and any variables created in scope and the return

when doSomething is completed this state is marked as no required however until the code can return the system has to keep allocating more and more room on the stack for each calls state this is why recursion is much less safe than normal loops

As commenters have already said, there is no stack involved in your example, or at least not how you imagine there is.

If you want to break your stack, try this instead:

function Overflow(count){
    console.log('count:', count)
    return Overflow(count + 1)
}

Overflow(0)
Related