자바스크립트는 원시값, 객체, 함수 등 우리가 만드는 모든 값들을 메모리에 저장한다. 이렇게 저장한 것들을 사용한 뒤 필요가 없어졌을 때 자동으로 메모리에서 제거하게 된다. 이를 가바지 컬렉션이라고 한다.
위에서도 말했듯이 필요가 없어진 값들은 자바스크립트가 알아서 관리해준다. 이 때문에 자바스크립트 개발자는 메모리 관리에 생각할 필요가 없다고 생각할 수 있는데 이는 잘못된 생각이다.
오히려 이를 알아야 개발을 진행하며 생기는 메모리 누수나 예기치 않은 에러 등을 방지하는데 도움을 줄 수 있다.
우선 가비지 컬렉션의 대상인 메모리를 먼저 알아보자.
메모리는 다음과 같은 생존 주기를 가진다.
개발자가 메모리를 할당해야하는 Low-level 언어와 달리 자바스크립트는 값을 선언할 때 자동으로 메모리를 할당한다. 원시 타입의 값들은 스택 영역에 저장되고, 참조 타입의 값은 힙에 저장되며, 그 주소값은 스택영역에 저장된다.
const a = 1; // 원시 타입은 메모리 스택 저장
const b = [1,2,3]; // 참조 타입은 힙 영역에 저장
function f(a) {
return a + 2;
} // 함수를 위한 할당(함수는 호출 가능한 오브젝트)
// 함수식 또한 오브젝트를 담기 위한 메모리를 할당한다.
someElement.addEventListener('click', function(){
someElement.style.backgroundColor = 'blue';
}, false);
// 함수 호출을 통한 할당
var d = new Date(); // Date 개체를 위해 메모리를 할당
var e = document.createElement('div'); // DOM 엘리먼트를 위해 메모리를 할당
// 메소드를 통한 할당
var a = ['ouais ouais', 'nan nan'];
var a2 = ['generation', 'nan nan'];
var a3 = a.concat(a2); // a 와 a2 를 이어붙여, 4개의 원소를 가진 새로운 배열
값을 사용한다는 것은 할당된 메모리를 읽고 쓰는 것을 의미한다.
console.log(a); // output: 1
console.log(b[0]); // output: 1
이 단계에서 대부분의 문제가 발생한다. 할당된 메모리가 더 이상 필요없을 때를 알아내는것이 어렵기 때문이다
low-level 언어에서는 개발자가 판단하고 결정하여 메모리를 해제한다. 하지만 자바스크립트는 개발자가 수동으로 할 수가 없기에 가비지 컬렉션(이하 GC)이라는 자동 메모리 관리 방법을 사용한다. 그렇다면 GC는 어떤 기준을 가지고 메모리를 필요없다고 판단할까?
GC의 핵심 개념은 참조이다. A라는 메모리를 통해 명시적이든 암시적이든 B라는 메모리에 접근할 수 있다면 B는 A에 참조된다
라고한다.
Reference-counting 알고리즘을 사용하는 GC는 필요없는 객체를 어떤 다른 객체도 참조하지 않는 객체
라고 정의한다. 이를 가비지
라 부르며, 메모리에서 삭제하게 된다.
let x = {
a: {
b: 2
}
};
let y = x; // 'y' 변수는 위의 오브젝트를 참조하는 두 번째 변수이다.
x = 1; // 이제 'y' 변수가 위의 오브젝트를 참조하는 유일한 변수가 됐다.
let z = y.a; // 위의 오브젝트의 'a' 속성을 참조했다.
// 이제 'y.a'는 두 개의 참조를 가진다.
// 'y'가 속성으로 참조하고 'z'라는 변수가 참조한다.
y = "apple"; // 이제 맨 처음 'y' 변수가 참조했던 오브젝트를 참조하는 오브젝트는 없다.
// 이제 오브젝트에 GC가 수행될까?
z = null; // 'z' 변수에 다른 값을 할당했다.
// 이제 맨 처음 'x' 변수가 참조했던 오브젝트를 참조하는
// 다른 변수는 없으므로 GC가 수행된다.
맨 처음 변수 x
에 객체를 할당하고 변수 y
는 x
를 참조하도록 한다. 그 후 x
에 새로운 값인 1을 재할당하면 객체는 메모리에서 제거될까? y
가 아직 객체를 참조하고 있기때문에 GC는 객체를 제거하지 못한다.
이제 새로운 변수 z
에 y
가 참조하는 객체의 a
를 할당해보자. 그리고 y
또한 새로운 값인 'apple'을 재할당하면 객체는 메모리에서 제거될까? z
가 내부 객체 a
를 참조하고 있기에 GC는 객체를 제거하지 못한다.
마지막으로z
에 null
을 재할당하게 되면 객체에 대한 참조가 존재하지 않게된다. 어떤 다른 객체도 참조하지 않는 객체
가 되므로 GC는 이를 필요없는 객체라 판단하여 메모리에서 제거하게 된다.
만약 객체가 순환 참조(객체와 객체가 서로 참조하는 구조)형태라면 Reference-counting 방식으론 GC가 불가능해진다.
function Circular() {
var x = {};
var y = {};
x.a = y; // x는 y를 참조한다.
y.a = x; // y는 x를 참조한다.
return "azerty";
}
Circular();
Circula
함수의 호출이 완료된 시점에서 객체 x
와 y
는 스코프를 벗어나게 될 것이고, 두 객체는 불필요해지므로 메모리에서 제거되어야한다. 하지만 함수 내부에서 두 객체가 서로 참조하고 있으므로 Reference-counting 알고리즘은 이를 GC의 대상으로 인식하지 못한다. 흔한 메모리 누수의 예제이다.
이를 해결하기 위해 Mark-and-sweep 알고리즘을 사용한다.
여기선 GC가 필요없는 객체를 닿을 수 없는 객체로 정의한다. 또한 roots라는 객체의 집합을 가지고 있다.(자바스크립트에선 전역 변수). roots에서 시작해서 roots가 참조하는 객체, 그리고 그 객체를 참조하는 객체들… 을 닿을 수 있는 객체라고 표시한다. GC는 닿을 수 없는 객체에 수행되게된다. 순서를 정리하자면 다음과 같다.