main

OS와 자바스크립트 엔진에서의 Heap과 Stack

11

이전에 프로세스와 스레드에 대해서 공부하면서 프로그램은 아직 실행되지 않은 프로그램이고, 프로세스는 실행되는 프로그램이라는 것을 알 수 있었습니다. 프로그램이 실행되면, CPU에서 메모리를 할당받게 됩니다. 이때 프로세스의 메모리는 4가지로 구성됩니다.

231011-135002

  • 프로세스의 메모리 영역
    • Code 영역: 실행할 프로그램의 코드가 저장되는 영역, CPU는 코드 영역에 저장된 명령어를 하나씩 가져가서 처리하게 됨.
    • Data 영역: 프로그램의 전역 변수와 정적 변수(Static) 저장되는 영역, 프로그램 시작과 함께 할당되며 종료되면 소멸됨.
    • Stack 영역: 함수호출과 관계된 지역변수와 매개변수가 저장되는 영역, 스택영역은 LIFO방식으로 작동함.
    • Heap 영역: 사용자가 직접 관리해야하는 메모리 영역. 사용자에 의해 동적으로 할당되고 해제됨.

OS에서 스택과 힙의 차이점

Stack (스택)

스택은 정적 메모리를 할당하고, 함수의 호출과 관계된 지역변수 매개변수가 저장됩니다. 함수 호출과 함께 할당되며, 함수 호출이 완료되면 소멸됩니다. LIFO방식으로 작동합니다.

Heap (힙)

동적 메모리 할당이 가능합니다. 사용자가 직접 관리할 수 있는 메모리입니다. 사용자에 의해 메모리 공간이 동적으로 할당되고 해제됩니다.


OS에서 스택과 힙의 장단점


Stack(스택)

장점단점
매우 빠른 액세스(할당, 해제가 빠름)메모리 크기 제한이 있음
변수를 명시적으로 할당 해제할 필요 X----

Heap(힙)

장점단점
변수는 전역적으로 접근할 수 있음상대적으로 느린 액세스(할당, 해제가 느림)
메모리 크기 제한이 없음메모리를 관리해야함(변수 할당, 해제 책임이 있음)
----운영체제마다 메모리 관리가 달라 어렵다

실제로 들여다보는 Heap과 Stack

실제로 C언어로 변수의 주소값을 알아내면서 더 쉽게 개념을 이해할 수 있습니다.

#include <stdio.h>
 
int n; // 전역변수
 
int fib(int k) {  // fib 함수와 매개변수 k 
    if(k<=1) return 1;
    return fib(k-1) + fib(k-2);
}
int main() {
    scanf("%d", &n);
    printf("%d", fib(n));
}

위와 같이 피보나치 수열을 구하는 함수를 구현하게 되었을 때, 전역변수인nfib함수는 낮은 주소의 값을 할당받게 되고 매개변수인 k는 높은 주소 값을 할당받게 됩니다. 왜냐하면 매개변수인 k는 스택공간에 저장되기 때문입니다.


자바스크립트 엔진에서의 콜스택과 힙

다른 언어에서의 스택이나 힙의 개념과 비슷하게 작동합니다. 이전에 프로세스는 여러가지의 스레드를 가질 수 있다고 한적이 있는데요. 그때마다 하나의 스택을 무조건 가진다고 포스팅을 한적이 있습니다. 반면에 JS는 너무 잘알다시피 싱글스레드 기반 언어입니다. 그렇기 때문에 하나의 CallStack을 가졌다는 의미이기도 합니다.

231011-142810

231011-142901

사진과 같이 자바스크립트는 런타임에 두 가지의 메모리 영역인 콜스택과 힙을 가집니다.
결과적으로 말하면 콜스택은 말 그대로 JS의 각 실행 컨텍스트를 불러와 call해 연산을 처리하고, 힙에서 동적인 데이터를 할당하게끔 유연한 메모리 할당과 최적화, 정렬 등을 담당합니다.

CallStack (호출 스택)

Call Stack은 말 그대로 스택을 call 하는 부분입니다. 그리고 JS에서 스택 하나하나는 모두 실행 컨텍스트입니다.
Stack은 LIFO ( Last In First Out )구조를 가지고 있습니다.

foo = (a,b) => {
  return a * b;
}
 
bar = (n) => {
  return foo(n ,n);
}
 
baz = (n) => {
  let result = bar(n);
  console.log(result);
}
 
baz(3)

231011-143137

위의 그림과 같이 함수의 실행 순서에 맞춰 콜스택이 쌓이게 되고, LIFO 순으로 콜스택이 제거 됩니다. 콜스택은 메모리의 크기가 제한되어있기 때문에 콜스택에 너무 많이 쌓여있게 되면 stackoverflow가 발생하게 됩니다.

각각의 함수가 자신이 정의된 스코프나 지역변수들을 어떻게 관리하는지에 대해서는 실행컨텍스트 - 렉시컬 환경 (환경 레코드, 외부 렉시컬 환경에 대한 참조)의 개념이 추가적으로 덧붙여지게 됩니다.

또한 콜스택에는 원시 타입의 데이터가 저장됩니다.


원시타입 데이터가 저장되는 콜스택

231011-144043

  • 원시 타입 데이터 (변수 a)
    • 10이라는 값 자체는 원시 타입이므로 콜 스택에 저장됩니다.
    • 변수 a에는 10이 저장된 콜 스택 메모리의 주소값이 저장됩니다.

  • 참조 타입 데이터 (변수 b,c,d)
    • 배열, 객체, 함수 등은 참조 타입이므로 메모리 힙에 저장됩니다.
    • 참조타입 데이터가 저장된 메모리 힙의 주소값은 콜스택에 각각 저장됩니다.
    • 메모리힙의 주소 값이 저장된 콜 스택의 주소값은 각각 변수 b, c, d에 저장됩니다.

원시타입 변수 생성

231011-144447
원시타입 데이터는 콜스택에 저장되고, 데이터 값이 저장된 콜스택의 주소 값은 변수 a와 b에 각각 저장됩니다.

만약에 원시타입을 재할당한다면 어떻게 될까요?

231011-144541
만약 변수 a에 값을 재할당하는 경우에는 본인의 메모리에 있는 값을 수정하는 것이 아니라, 기존에 20을 저장하고 있는 메모리의 주소 값으로 교체하게됩니다. a에 저장된 주소 값은 20을 가리키고 있던 b에 저장된 주소 값과 동일해집니다.
만약에 기존에 20이 없다고 한다면 새로운 메모리를 확보하고 그곳에 20을 저장합니다. 그리고 a는 새로운 주소 값을 가르키게 됩니다. 이후 참조되지 않는 값이 있다면 가비지 컬렉터 대상이 됩니다.


참조타입 변수 생성

let myArray = [];

위와 같이 새로운 객체 변수를 선언하고 빈배열로 초기화 한다면 어떻게 참조타입 변수가 생성될까요?

  • 변수의 고유 식별자를 생성합니다. ("myArray")
  • 콜스택의 메모리에 식별자를 할당합니다. (런타임에 할당됨)
  • 힙에 할당된 메모리 주소를 콜스택의 값으로 저장합니다. (런타임에 저장)
  • 힙의 메모리 주소에 할당된 값을 저장합니다. (빈 배열: [])

231011-145020
231011-145154

const키워드를 사용해 생성한 객체나 배열에는 push, pop 메서드를 사용해도 값이 변하는 것을 알 수 있습니다.
기본적으로 "변경"이란 메모리의 주소를 변경하는 것을 말합니다.
let 은 메모리 주소를 변경을 허락합니다.
const 는 메모리 주소를 변경할 수 없습니다.

pushpop 메서드를 사용해서 배열을 수정해도, 콜스택이 참조하고 있는 힙의 메모리 주소는 변하지 않기 때문입니다. 따라서 메모리 주소 값이 변하지 않는다고 생각하기 때문에 배열과 객체를 수정할 수 있는 것입니다.

반대로 const로 생성한 원시 타입의 데이터는 재할당되는 경우, 메모리의 주소값을 변동해야하기 때문에 재할당이 불가한 것입니다.

Reference Doc

스택(Stack)과 힙(Heap) 차이점
힙 영역 vs 스택 영역
힙과 스택
자바 메모리 관리 스택(stack) & 힙(heap) 영역 설명, 예시


김다은 이모지
Daeun Kim
Junior Frontend Engineer