메모리 수명 주기(Memory Life Cycle)
프로그래밍 언어의 종류와 관계없이 메모리 수명 주기는 거의 비슷한 패턴을 따릅니다.
메모리 수명 주기의 3단계
- 필요한 메모리를 할당(Allocation)합니다.
- 할당된 메모리를 사용(Use)합니다(읽기 또는 쓰기).
- 더 이상 필요하지 않을 때 할당된 메모리를 해제(Release)합니다.
여기서 첫 번째 단계와 마지막 단계는 C 언어 같은 저수준 언어에서는 개발자가 직접 처리해야 하지만, JavaScript처럼 고수준 언어에서는 엔진이 간접적으로(자동으로) 처리해 준다는 점이 큰 차이입니다.
1) JavaScript에서의 메모리 할당
JavaScript는 가비지 컬렉션(garbage collected) 언어입니다. 즉, 변수를 선언하면 자동으로 해당 변수에 메모리가 할당되며, 선언된 변수에 대한 참조가 더 이상 존재하지 않으면 할당된 메모리는 자동으로 해제됩니다.
예제
다음 예제에서 JavaScript는 숫자 하나, 문자열 하나, 그리고 객체 하나에 대해 각각 메모리를 할당합니다.
var n = 989; // 숫자(number)를 위한 메모리 할당
var s = 'qwerty'; // 문자열(string)을 위한 메모리 할당
var o = {
a: 1,
b: null
}; // 객체(object)와 포함된 값들을 위한 메모리 할당2) 할당된 값 사용하기
값을 사용한다는 것은 기본적으로 할당된 메모리를 읽고 쓰는 행위를 의미합니다. 변수나 객체 속성(property)의 값을 읽거나 쓰는 것, 그리고 함수에 인자(argument)를 전달하는 것까지 모두 여기에 해당합니다.
3) 불필요해진 메모리 해제하기
대부분의 메모리 관리 문제는 바로 이 단계에서 발생합니다. 가장 어려운 작업은 "할당된 메모리가 더 이상 필요 없는 시점"을 정확히 파악하는 것입니다. 이 문제를 해결하기 위해 대부분의 고수준 언어에는 가비지 컬렉터(garbage collector)라는 소프트웨어가 내장되어 있습니다.
가비지 컬렉터의 역할은 메모리 할당을 추적하고, 더 이상 필요하지 않게 된 메모리를 찾아내어 해제하는 것입니다. 그러나 안타깝게도 이 과정은 어디까지나 '추정'에 불과합니다. 특정 메모리 조각이 앞으로도 필요한지 여부를 판별하는 문제는 결정 불가능(undecidable)한 문제이기 때문입니다(알고리즘이 완벽하게 추적해 낼 수 없습니다).
JavaScript의 가비지 컬렉터는 참조 카운팅(Reference-counting) 가비지 컬렉션과 같은 알고리즘을 활용하여 더 이상 사용되지 않는 메모리를 찾아냅니다.
참조 카운팅의 한계와 마크 앤 스위프(Mark-and-Sweep)
참조 카운팅 방식은 각 객체를 참조하는 개수를 세어, 참조 수가 0이 되면 해당 메모리를 회수하는 방식입니다. 다만 두 객체가 서로를 참조하는 순환 참조(circular reference) 상황에서는 외부에서 아무도 참조하지 않아도 참조 수가 0이 되지 않아 메모리 누수(memory leak)가 발생할 수 있다는 한계가 있습니다.
이러한 한계를 보완하기 위해 V8을 포함한 현대 JavaScript 엔진은 대부분 마크 앤 스위프(Mark-and-Sweep) 알고리즘을 사용합니다. 이 방식은 루트(root)에서 시작해 도달 가능한(reachable) 객체를 표시(mark)하고, 도달할 수 없는 객체를 쓰레기로 간주하여 한꺼번에 회수(sweep)함으로써 순환 참조 문제도 깔끔하게 해결합니다.