메모리 관리를 크게 메모리 할당과 해제, 두 가지 과정으로 나눠 살펴보자.
C와 같은 저수준 언어에서는 malloc()
, free()
와 같은 함수를 통해 개발자가 직접 메모리를 할당하고 해제한다. 하지만 자바스크립트에서는 이러한 작업이 자바스크립트 엔진을 통해 자동으로 이루어진다.
메모리 할당은 변수나 객체를 생성할 때 발생한다.
예를 들어 const name = “maeil-mail”
과 같은 코드에서 name
변수는 메모리에 할당된다.
문자열, 숫자와 같은 원시 값은 Stack 영역에 저장된다. 원시 값은 고정 크기를 가지기 때문에, 엔진이 컴파일 타임에 그 크기를 알고 있어 정적 데이터라고 불린다. 또한 원시 값에 고정된 메모리를 할당하기 때문에 정적 메모리 할당이라고 부른다.
한편 객체(참조 타입)는 Heap 영역에 저장된다. 객체는 실행 시점에 필요한 만큼 메모리가 동적으로 할당되기 때문에 동적 데이터, 동적 메모리 할당이라고 불린다.
➡️ 변수의 스코프, 실행 컨텍스트, 렉시컬 환경 추가 학습하기❗️
메모리 해제는 할당된 메모리가 더 이상 필요 없을 때 발생한다.
자바스크립트는 가비지 컬렉션(GC)
이라는 자동 메모리 해제 방법을 사용한다. 언어 차원에서 메모리 할당을 추적하고, 특정 메모리가 필요하지 않게 되었다면 메모리를 해제하는 방식이다.
대표적인 가비지 컬렉션 알고리즘인 Reference-counting과 Mark-and-sweep에 대해 간단히 알아보자.
1️⃣ Reference-counting은 단순하게 구현된 알고리즘으로, 객체가 참조되는 횟수를 추적하고 참조 횟수가 0이 되면 메모리에서 해제한다. 즉 다른 어떤 객체도 참조하지 않는 객체를 찾아내어 메모리를 해제하는 방식이다.
하지만 이 방식에서는 순환 참조가 발생할 경우 참조 카운트가 0이 되지 않아 메모리에서 해제되지 않는 문제가 발생하는 한계점이 있다. 예를 들어 위 그림을 보자.
foo
변수와 bar
변수의 참조를 해제하였을 때, 두 객체는 서로 순환 참조를 하고 있고 카운트가 1이므로, 가비지 컬렉터의 수거 대상이 되지 않는다.
두 객체는 변수에서도 접근하지 못해 메모리 힙 영역에 영원히 남아있게 되어, 메모리 누수의 원인이 된다.
2️⃣ Mark-and-sweep은 루트 객체에서 시작해 참조되는 객체를 표시(mark)하고, 마지막까지 표시되지 않은 객체를 찾아 메모리를 청소(sweep), 해제하는 방식이다. 이 방식에서는 순환 참조가 발생하더라도 루트에서 도달이 불가능하다면 표시가 되지 않는다. 이런 특징으로 인해 Reference-counting 방식의 한계점을 보완할 수 있다.
가비지 컬렉션은 메인 스레드에서 발생하기 때문에, 자바스크립트가 실행되지 않는데, 이 현상을 stop-the-world 현상이라고 한다.
이 현상을 해결하기 위해 V8 엔지니어들이 도입한 다음 네 가지 방식을 살펴보자.
1️⃣ Parallel, 병렬적 방식
메인 스레드 혼자 하던 일을 헬퍼 스레드들과 균등하게 나누어 일을 한다. 스레드 간의 동기화를 처리해야 해서 오버헤드는 생기지만, stop-the-world 시간이 크게 줄어든다.
2️⃣ Incremental, 점진적 방식
가비지 컬렉션의 작업을 나누어서 처리한다.
메인 스레드에서 가비지 컬렉션에 소요하는 시간이 분산되어, 좋은 UX를 제공할 수 있다.
3️⃣ Concurrent, 동시성 방식
메인 스레드는 더 이상 가비지 컬렉션을 하지 않고, 헬퍼 스레드들이 이를 수행한다. 기술적으로 구현하기에는 어렵지만, 메인 스레드의 stop-the-world 시간이 전혀 없다는 큰 장점이 있다.
4️⃣ Idle-time GC, 유휴시간 가비지 컬렉션
브라우저에서의 렌더링 과정에서는 애니메이션 프레임이 한 프레임 당 16.7ms가 되는데, 애니메이션 프레임 렌더링 작업이 16.7ms보다 빨리 끝나면 다음 프레임 작업 전까지 가비지 컬렉션을 유발한다.
🎥 [10분 테코톡] 코난의 자바스크립트 메모리 관리
✍🏻 Trash talk: the Orinoco garbage collector
잘 읽었습니다! 감사합니다!