October 09, 2021
가비지컬렉터의 역할은 자바스크립트 엔진이 메모리 할당을 모니터링하고 할당된 메모리의 블록이 더 이상 필요하지 않은 시점을 확인하여 회수하는 것이다.
가비지컬렉터는 Reference-counting
알고리즘과 Mark-and-sweep
알고리즘에 따라 동작한다.
참조-세기(Reference-counting)
가비지
라고 부르며, 이를 참조하는 다른 오브젝트가 하나도 없는 경우 수집이 가능하다.표시하고-쓸기(Mark-and-sweep)
mark bit
가 0(false)
으로 설정된다. mark
단계에서 접근 가능한 객체의 mark bit
가 1(true)
로 설정된다.mark
단계 후 mark bit
가 여전히 0
으로 설정된 객체들은 도달할 수 없는 객체이므로 가비지컬렉터가 수집해 메모리에서 해제된다.참조-세기
알고리즘의 문제점을 보완할 수 있어 2012년 이후 대부분의 브라우저에서 채택하고 있다.가비지컬렉션은 자동으로 실행되며 강제로 멈추거나 실행시킬 수 없다.
C 언어 같은 저수준(로우레벨) 언어에서는 메모리 관리를 위해 malloc()
과 free()
를 사용한다고 합니다. 반면에, 자바스크립트는 눈에 보이지 않는 곳에서 메모리 관리를 수행합니다.
객체가 생성되었을 때 자동으로 메모리를 할당하고 쓸모 없어졌을 때 자동으로 해제합니다.(가비지 컬렉션)
원시값, 객체, 함수 등 우리가 만드는 모든 것은 메모리를 차지합니다. 그럼 쓸모 없어지게 된 것들은 자동으로 해제 된다는데 어떤 기준에 의해 해제되는 것일까요?
자바스크립트의 가비지컬렉션 기준을 알아보기 전에 메모리의 생존주기와 자바스크립트에서의 메모리 할당 대해 알아보겠습니다.
메모리의 생존주기는 저수준 언어, 고수준 언어와 관계없이 비슷합니다.
- 필요할 때 할당한다.
- 사용한다. (읽기, 쓰기)
- 필요없어지면 해제한다.
2번은 모든 언어에서 명시적으로 사용되지만 1번과 3번은 저수준 언어에서는 명시적이며, 자바스크립트와 같은 고수준(하이레벨) 언어에서는 암묵적으로 작동합니다.
프로그래머가 일일이 메모리를 할당 하지 않도록 하기 위해서 자바스크립트는 값 초기화를 할 때 자동으로 메모리를 할당합니다.
var num = 123; // 정수를 담기 위한 메모리 할당
var str = '123'; // 문자열을 담기 위한 메모리 할당
var obj = {
a: 123,
b: null,
}; // 객체와 객체에 포함된 값들을 담기 위한 메모리 할당
var arr = [123, null, '123']; // 배열과 배열에 담긴 값들을 위한 메모리 할당
function foo(a) {
return a + 1;
} // 함수를 위한 할당(함수는 호출 가능한 객체입니다)
// 함수표현식 또한 객체를 담기위한 메모리를 할당합니다
someElement.addEventListener('click', function() {
someElement.style.backgroundColor = 'red';
}, false);
함수 호출의 결과 메모리 할당이 일어나기도 합니다.
var date = new Date(); // Date 객체를 위해 메모리 할당
var el = document.createElement('div'); // DOM Element를 위해 메모리 할당
메소드가 새로운 값이나 객체를 할당하기도 합니다.
var str = '123';
var str2 = str.substr(0, 2); // str2 새로운 문자열
// 자바스크립트에서 문자열은 immutable 값이기 때문에
// 메모리를 새로 할당하지 않고 단순히 [0, 2] 이라는 범위만 저장합니다.
var arr = ['123', '456'];
var arr2 = ['789', '101112'];
var arr3 = arr.concat(arr2);
// arr과 arr2를 합친 4개의 원소를 가진 새로운 배열
쉽게 말하면 어떤 값들이 더 이상 도달이 불가능한 경우 가비지컬렉션의 대상이 됩니다.
자바스크립트는 도달 가능성(reachability)
이라는 개념을 사용해 메모리 관리를 수행합니다.
도달 가능한 값은 쉽게 말해 어떻게든 접근하거나 사용할 수 있는 값을 의미합니다. 도달 가능한 값은 메모리에서 삭제되지 않습니다.
아래의 값들은 태생부터 도달 가능하기 때문에 이유 없이 삭제되지 않습니다.
이런 값은 루트(root)
라고 부릅니다.
루트
가 참조하는 값이나 체이닝으로 루트에서 참조할 수 있는 값은 도달 가능한 값이 됩니다.
전역 변수에 객체가 저장되어있다고 가정해보면, 이 객체의 프로퍼티가 또 다른 객체를 참조하고 있다면, 프로퍼티가 참조하는 객체는 도달 가능한 값이 됩니다. 따라서 이 객체가 참조하는 다른 모든 것들도 도달 가능하다고 여겨집니다.
// user엔 객체 참조 값이 저장됩니다.
let user = {
name: 'John'
};
이 그림에서 화살표는 객체 참조를 나타냅니다. 전역 변수 user
는 { name: 'John' }
이라는 객체를 참조합니다.
user
의 값을 다른 값으로 덮어쓰면 참조가 사라집니다.
user = null;
John은 도달할 수 없는 상태가 되었기 때문에 가비지 컬렉터(이하 GC)가 John에 저장된 데이터를 삭제하고, John을 메모리상에서 삭제합니다.
참조를 user
에서 admin
으로 복사했다고 가정해봅시다.
// user엔 객체 참조 값이 저장됩니다.
let user = {
name: 'John'
};
let admin = user;
그리고 위에서 한 것 처럼 user
의 값을 다른 값으로 덮어써 봅니다.
user = null;
전역 변수 admin
을 통하면 여전히 객체 John에 접근할 수 있기 때문에 John은 메모리상에서 삭제되지 않습니다. 이 상태에서 admin
을 다른 값으로 덮어쓰면 John은 메모리상에서 삭제될 수 있습니다.
조금 복잡한 예시가 있습니다.
function marry(man, woman) {
woman.husband = man;
man.wife = woman;
return {
father: man,
mother: woman,
}
};
let family = marry({
name: 'John',
}, {
name: 'Ann',
});
메모리 구조는 아래와 같이 나타낼 수 있습니다.
위 예시의 함수는 호출이되고 끝나서 더 이상 필요한 값이 아닌데도 파라미터로 받은 두 객체를 서로 참조하게 되면서(순환 참조) GC는 이 값들에 대한 메모리를 삭제하지 않아서 메모리에 계속 남아있게 됩니다. 순환참조는 메모리 누수를 일으키는 주된 요인이라고 할 수 있습니다.
참조 두 개를 지워보도록 하겠습니다.
delete family.father;
delete family.mother.husband;
삭제한 두 개의 참조 중 하나만 지웠다면, 모든 객체가 여전히 도달 가능한 상태였지만 두 개를 지우면 John으로 들어오는 참조는 모두 사라져 John은 도달 가능한 상태에서 벗어나 GC에 의해 메모리상에서 삭제됩니다.
객체들이 연결되어 섬 같은 구조를 만드는데, 이 섬에 도달할 수 없으면 섬을 구성하는 객체 전부 메모리상에서 삭제됩니다.
family
가 아무것도 참조하지 않도록 만들어 봅시다.
family = null;
John과 Ann은 여전히 서로를 참조하고 있고, 두 객체 모두 외부에서 들어오는 참조를 가지고 있습니다.
하지만 fmaily
객체와 루트의 연결이 사라지면 루트 객체를 참조하는 것이 아무것도 없게 됩니다. 섬 전체가 도달할 수 없는 상태가 되어
섬을 구성하는 객체 모두가 메모리상에서 삭제됩니다.
도달할 수 없는 섬 예제는 도달 가능성이라는 개념이 얼마나 중요한지 보여줍니다.
Mark-and-sweep
알고리즘은 다음 단계를 거쳐 수행됩니다.
mark(표시)
합니다.mark
합니다.mark
된 모든 객체에 방문하고 그 객체들이 참조하는 객체도 mark
합니다. 한번 방문한 객체는 전부 mark
하기 때문에 같은 객체를 다시 방문하지는 않습니다.mark
되지 않은 모든 객체를 메모리상에서 삭제합니다.function couple() {
const John = {};
const Ann = {};
// John.girlFriend는 Ann을 참조한다.
John.girlFriend = Ann;
// Ann.boyFriend는 John을 참조한다.
Ann.boyFriend = John;
return '순환참조';
};
couple();
위 예시에서 couple()
이라는 함수가 호출된 후 '순환참조'
가 return 되고 함수가 끝난 후에는 더 이상 root에서 John과 Ann에 도달할 수 없기 때문에
해당 값들은 GC에 의해서 메모리상에서 삭제됩니다.
2012년부터 모던 브라우저들은 대부분 GC에 Mark-and-sweep
알고리즘을 사용합니다.