JavaScript stack size

Finding the size of the memory allocated for the stack.

Variables in JavaScript (and most other programming languages) are stored in two places: stack and heap. A stack is usually a continuous region of memory allocating local context for each executing function. Heap is a much larger region storing everything allocated dynamically. This separation is useful to make the execution safer from corruption (stack is more protected) and faster (no need for dynamic garbage collection of the stack frames, fast new frame allocation).

A typical stack example

1
2
3
function foo() { var a = 1; }
function bar() { var b = 2; foo(); }
bar();

stack when the execution is paused inside foo

start of the stack
---------------------------
bar local frame
  * a couple of OS pointers
  * local variable b (2)
  * place for returned value
  * pointer to next frame
---------------------------
foo local frame
  * a couple of OS pointers
  * local variable a (1)
  * place for returned value

Even if a function calls itself recursively, each frame has its own copy of all local variables.

When a function finishes execution, its frame is removed from the stack, freeing memory allocated by all local variables. This is why even in languages like C or C++ you never have to worry about freeing local variables.

Stack size

We can try calculating size of the stack. If a recursive function allocates too many frames on the stack, the JavaScript environment throws an exception [RangeError: Maximum call stack size exceeded]. Under Node 0.11.10 x64 the following recursive program can report how many times a small function executes before dying

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var counter = 0;
try {
function foo() {
counter += 1;
foo();
}
foo();
} catch(e) {
console.error(e);
console.log('counter =', counter);
}
// output
[RangeError: Maximum call stack size exceeded]
counter = 20961

Lets increase the size of each frame by having a single local variable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var counter = 0;
try {
function foo() {
var local = 1;
counter += 1;
foo();
}
foo();
} catch(e) {
console.error(e);
console.log('counter =', counter);
}
// output
[RangeError: Maximum call stack size exceeded]
counter = 17967

A number in JavaScript takes up 8 bytes, this allows us to calculate the maximum stack size and the size of each frame

N - size of single stack frame
20961 * N = 17967 * (N + 8)
(20961 - 17967) * N = 17967 * 8
2994 * N = 143736
N = 143736 / 2994 = 48 bytes = 6 numbers
Total stack size = 20961 * 48 = 1006128 < 1MB

Without any local variables, each function call takes up 48 bytes during the execution, and you are limited to less than 1MB for all local function frames.

We can confirm the stack size by adding second local variable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var counter = 0;
try {
function foo() {
var local = 1, local2 = true;
counter += 1;
foo();
}
foo();
} catch(e) {
console.error(e);
console.log('counter =', counter);
}
// output
[RangeError: Maximum call stack size exceeded]
counter = 15721

Using the previous total stack size and dividing by the counter

1006128 / 15721 = 64 bytes per frame
64 = 48 + 8 (for local) + 8 (for local2)

Each boolean and number variable takes 8 bytes of memory.

Heap variables

Only primitive types passed by value (Number, Boolean, references to objecs) are stored on the stack. Everything else is allocated dynamically from the shared pool of memory called heap. In JavaScript you do not have to worry about deallocating objects inside the heap, the garbage collector frees them whenever no one is referencing them. Of course, creating large number of objects takes its performance toll (someone needs to keep all the bookkeeping) plus memory fragmentation.

You can see that objects are not allocated on the stack by changing the type of the local variable from a number to an object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var counter = 0;
try {
function foo() {
var local = {
foo: 'foo'
};
counter += 1;
foo();
}
foo();
} catch(e) {
console.error(e);
console.log('counter =', counter);
}
// output
[RangeError: Maximum call stack size exceeded]
counter = 17967

Notice that having a local object {foo: 'foo'} has not affected the number of frames on the stack before we ran out of space. This is because only the local variable is stored in each frame pointing at objects allocated on the heap. Same for functions, dates, arrays and other objects passed by reference, including strings.