JS 메모리 구조

불꽃남자·2020년 9월 19일
6

오늘은 JS의 메모리 구조에 대해 기본적인 것들만 알아보자.
전체적으로 Chidum Nnamdi - Understanding Memory Leaks in Nodejs를 참고하여 작성하였다. 거의 그대로 번역했다.

메모리 구조

JS의 주소 공간

아래의 그림은 프로그램이 실행될 때에, JS 엔진이 가지는 세 개의 메모리 공간(Code Area, Call Stack, Heap)을 시각화 한 그림이다. 이것들을 하나로 묶어 프로그램의 주소 공간이라고 한다.
메모리 힙 1
이 녀석들은 무언가를 저장한다는 공통점을 갖고 있다.

Code Area는 실행할 JS 코드를 저장한다.
Call Stack은 실행 중인 함수를 추적하며 계산을 수행하고, 지역 변수를 저장한다. 변수들은 LIFO 형식으로 저장된다. 또한 원시 타입들이 이 곳에 저장된다.
Heap은 참조 타입들이 할당되는 곳이다. 콜 스택과 달리, Heap의 메모리 할당은 LIFO 정책을 따르지 않고 랜덤하게 배치된다. 또한 메모리 누수를 방지하기 위해 JS 엔진의 메모리 관리자가 항상 관리한다. (가비지 컬렉션에 대한 이야기이다.)

아래의 예시를 살펴보자.

function Animal() {};
// stores `new Animal()` instance on memory address 0x001232
// tiger has 0x001232 as value in stack
const tiger = new Animal();
// stores `new Object()` instance on memory address 0x000001
// `lion` has 0x000001 as value on stack
let lion = {
    strength: "Very Strong"
}

메모리 힙 2
tiger와 lion은 는 참조형 타입이다.
코드가 실행되다 Animal()의 인스턴스를 생성하면 Heap의 메모리 주소 어딘가에 저장된다. 그리고 변수 tiger은 Stack에 쌓이게 되고 그 value로 Animal()의 Heap 메모리 주소를 갖게 된다. lion도 마찬가지이다.
즉, tiger 변수에 접근하면 value가 가리키고 있는 Heap 메모리 주소로 이동하고 거기에 저장된 value를 꺼내오게 되는 것이다.

Stack에 변수가 쌓인 순서와 Heap에 인스턴스가 저장된 순서가 다른 것은, Heap의 참조 타입 데이터는 메모리 어딘가에 랜덤하게 할당된다는 것을 나타내고자 한 것 같다.

좀 더 자세히 들여다보면 Heap 내부에도 여러가지 Space들이 존재하며, 참조형 데이터의 역할과 종류에 따라 다른 Space의 메모리에 할당된다.

메모리 힙 3
사진 출처 - TOAST UI

이에 대해서는 다른 포스팅에서 알아보자.

가비지 컬렉션 (Garbage Collection)

메모리의 생명 주기

프로그래밍 언어와 관계없이, 메모리 생성 주기는 아래와 같다.

  1. 필요 메모리 할당
  2. 메모리 사용
  3. 필요 없어진 메모리 제거

해당 프로그래밍 언어가 low-level의 언어는 개발자가 직접 메모리를 관리해야 한다. 하지만 high-level의 언어는 변수를 초기화 할 때에 자동으로 메모리에 할당하고, 더 이상 필요 없다고 판단되면 메모리에서 제거한다.

그리고 변수의 필요유무를 판단하고 메모리에서 제거하는 녀석이 바로 가비지 컬렉션이다.
즉, 가비지 컬렉션은 자동 메모리 관리자 정도가 되겠다.

자동으로 메모리가 관리된다는 것은 개발자가 직접 메모리를 관리하는 수고로움을 갖지 않아도 된다는 장점이 있으나, 이것은 반대로 말하면 개발자가 메모리에 대한 제어권을 잃는다는 의미이기도 하다.

가비지 컬렉션의 작동 원리(Mark-Sweep)

사실 Heap 내부에는 세 개 이상의 가비지 컬렉션이 존재하고, 각각 다른 내부 알고리즘을 사용하여 메모리를 최적화시킨다.

여기에서는 가장 흔히 알려진 Mark-Sweep 알고리즘을 통해 가비지 컬렉션의 작동 원리에 대해 간단히 알아본다.

Mark-Sweep 알고리즘은 우선 roots라는 root 객체의 집합을 만들어 낸다. JS에서 roots는 전역 변수들이다. 그리고 주기적으로 roots로부터 접근 가능한 모든 객체들을 돌아다니며 Marking한다. Marking이 끝나고 메모리 내에 Marking이 되지 않은 데이터들은 사용하지 않는 데이터로 판단해 지워버린다.(Sweeping)

이것이 Mark-Sweep 가비지 컬렉션의 기본적인 동작 원리이다.

마치며

JS(V8 엔진)의 전체적인 메모리 구조를 알아보고, 그 안에서 메모리 힙과 콜 스택의 차이를 짚으며 자연스럽게 가비지 컬렉션에 대해서도 알아보는 것이 처음 이 글의 목적이었다.
그런데 글을 쓰려고 알아보고 있으려니 메모리 구조는 꽤 많이 복잡했고, 그만큼 가비지 컬렉션 또한 복잡했다.
세세한 부분까지 알아보며 글을 쓰려고 했으나, 그러기엔 너무 시간이 많이 걸려 정말 기본적인 부분만을 짚어보았다. 조바심을 내면 안 되지만, 하루 빨리 JS에 대한 지식을 쌓고 React에 대해 알아보고 싶기 때문이다.

profile
프론트엔드 꿈나무, 탐구자.

1개의 댓글

comment-user-thumbnail
2021년 4월 19일

다들 call stack만 아는데, heap 부분은 처음보는거 같아요 감사합니다

답글 달기