JavaScript는 객체가 생성되었을 때 자동으로 메모리를 할당하고 더 이상 필요하지 않을 때 자동으로 해제합니다(가비지 컬렉션). 이러한 자동 메모리 관리는 잠재적 혼란의 원인이기도 한데, 개발자가 메모리 관리에 대해 고민할 필요가 없다는 잘못된 인식을 심어줄 수 있기 때문입니다.
메모리 생존주기
메모리 생존주기는 대부분의 프로그래밍 언어에서 비슷
- 필요할 때 할당합니다.
- 할당된 메모리를 사용합니다. (읽기, 쓰기)
- 더 이상 필요하지 않으면 해제합니다.
두 번째 부분은 모든 언어에서 명시적으로 사용됩니다. 그러나 첫 번째 부분과 마지막 부분은 저수준 언어에서는 명시적이며, JavaScript와 같은 대부분의 고수준 언어에서는 암묵적으로 작동합니다.
- 값 할당
- 함수 호출을 통한 할당
- 값 사용
가비지 콜렉션
위에서 언급한 것처럼 “더 이상 필요하지 않은” 모든 메모리를 찾는건 비결정적 문제입니다. 따라서 가비지 컬렉터들은 이 문제에 대한 제한적인 해결책을 구현합니다. 이 섹션에서는 주요한 가비지 컬렉션 알고리즘들과 그 한계를 이해하는데 필요한 개념을 설명합니다.
참조-세기(Reference-counting) 가비지 콜렉션
참고로 최신 브라우저는 이 방식을 사용 안함 이 알고리즘은 ‘어떤 다른 객체도 참조하지 않는 객체’를 ‘더 이상 필요 없는 객체’라고 여깁니다.이 객체를 “가비지”라 부르며, 이를 참조하는 다른 객체가 하나도 없는 경우, 수집이 가능합니다.
함수 호출이 완료되면 이 두 객체는 스코프를 벗어나게 될 것이며, 그 시점에서 두 객체는 불필요해지므로 할당된 메모리는 회수되어야 합니다. 그러나 두 객체가 서로를 참조하고 있으므로, 참조-세기 알고리즘은 둘 다 가비지 컬렉션의 대상으로 표시하지 않습니다. 이러한 순환 참조는 메모리 누수의 흔한 원인입니다.
function f() {
const x = {};
const y = {};
x.a = y; // x는 y를 참조합니다.
y.a = x; // y는 x를 참조합니다.
return "azerty";
}
f();
표시하고-쓸기(Mark-and-sweep) 알고리즘
이 알고리즘은 “roots” 라는 객체의 집합을 가지고 있습니다. JavaScript에서 root는 전역 객체입니다. 주기적으로, 가비지 콜렉터는 roots로 부터 시작하여 roots가 참조하는 객체들, roots가 참조하는 객체가 참조하는 객체들 등을 찾습니다. roots로 부터 시작하여 가비지 콜렉터는 모든 도달할 수 있는 객체들을 찾고, 도달할 수 없는 모든 객체들을 수집합니다.
참조가 없는 객체는 명확히 도달할 수 없기 때문입니다.
현재 모든 최신 엔진은 표시하고-쓸기 가비지 수집을 제공합니다. JavaScript 가비지 수집 필드(세대별/증분적/동시적/병렬적 가비지 수집)에서 지난 몇 년간의 모든 개선들은 이 알고리즘의 구현을 통한 개선이며, 가비지 수집 알고리즘이나 언제 “객체가 필요 없는지”에 대한 정의를 반영하는 부분에 있어서의 개선은 아닙니다. 가비지 수집을 수동으로 조작할 수 없다
엔진의 메모리 모델(memory model) 설정하기
JavaScript 엔진은 주로 메모리 모델을 노출하는 플래그를 제공합니다. 예로, Node.js는 설정과 메모리 문제 디버깅을 위해 내부를 구성하는 V8 메커니즘을 노출하는 추가적인 옵션과 도구를 제공합니다. 이 설정은 브라우저에서는 대부분은 불가능하고, 웹 페이지(HTTP 헤더 등을 통해) 상에서는 더더욱 불가능합니다.
가용한 힙 메모리의 최대량은 아래와 같은 플래그를 통해 올릴 수 있습니다:
BASHCopy to Clipboard
node --max-old-space-size=6000 index.js
또한 플래그나 Chrome Debugger를 사용해 메모리 문제를 디버깅하기 위한 가비지 컬렉터 정보를 보여줄 수 있습니다:
BASHCopy to Clipboard
node --expose-gc --inspect index.js
메모리 관리를 돕는 데이터 구조
WeakMaps과 WeakSets
주로 WeakMap
과 WeakSet
을 설명할 때, 보통 키가 먼저 가비지 수집되고 이후 값 또한 가비지 수집된다고 암시합니다. 그러나, 아래와 같이 키를 참조하는 값이 있는 케이스를 살펴보겠습니다.
const wm = new WeakMap();
const key = {};
wm.set(key, { key });
// 값이 키를 참조하기에, `key`는 가비지 콜렉션 대상이 아니며
// 그 값은 map 안에서 strongly hold되어 있습니다.
만약 key
가 실제 참조로 저장된다면, 다른 값이 key
를 참조하지 않아도 순환 참조를 만들며 키와 값 모두 가비지 수집 대상이 아니도록 합니다.