JavaScript는 눈에 보이지 않는 곳에서 메모리 관리를 수행합니다.
원시 값, 객체, 함수 등 우리가 만드는 모든 것은 메모리를 차지합니다. 글로벌 컨텍스트에서 각각의 함수 컨텍스트가 끝나면, 더 이상 필요 없어진 메모리를 처리하는 것이 가비지 컬렉션입니다.
JavaScript에서는 도달가능성(reachability)이라는 개념을 사용해 메모리 관리를 수행합니다. 즉, 가비지 콜렉션 알고리즘의 핵심 개념은 참조 입니다. A라는 메모리를 통해 B라는 메모리에 접근할 수 있다면 B는 A에 참조되며
참조-세기(Reference-counting) 가비지 콜렉션
참조-세기 알고리즘은 "더 이상 필요없는 객체"를 "어떤 다른 객체도 참조하지 않는 객체"라고 정의합니다. 이 객체를 "가비지"라 부르며, 이를 참조하는 다른 객체가 하나도 없는 경우, 수집이 가능합니다.
var x = { //x 선언 origin
a: {
b: 2
}
};
var y = x; // y의 x 참조
x = 1;
// x가 1을 가리켜도 y가 가리키고 있기에 origin이 아직 존재함.
var z = y.a;
// z의 origin.a 객체를 참조
y = "roothyo"
// y가 String literal을 가리켜도 z가 origin.a를 참조하기에 아직 존재함.
z = null;
// 비로소 origin 제거
※ 한계 : 순환 참조
참조-세기 알고리즘은 순환 참조를 다루는 일에는 한계가 있습니다. 예제에서 두 객체가 서로 참조하는 속성으로 생성되어 순환 구조를 생성합니다. 함수 호출이 완료되면 두 객체는 할당된 메모리가 회수되어야 하지만 두 객체가 서로를 참조하고 있으므로 참조-세기 알고리즘은 가비지 컬렉션 대상으로 표시하지 않습니다. 이는 메모리 누수의 흔한 원인중 하나입니다.
function f(){
var x = {}
var y = {}
x.a = y;
y.a = x;
return "azerty";
}
f();
마크 앤 스윕(Mark-and-Sweep) 알고리즘
: "더 이상 필요 없는 객체"를 "닿을 수 없는 객체"로 정의합니다. 이 알고리즘은 roots라는 객체의 집합(JS에서는 전역변수들)을 갖고 있습니다. 주기적으로 가비지 콜렉터는 roots로 부터 시작하여 roots가 참조하는 객체들, roots가 참조하는 객체가 참조하는 객체들 등 닿을 수 있는 객체가 아닌 닿을 수 없는 객체에 대해 가비지 콜렉션을 수행합니다.
"참조 되지 않는 오브젝트" => "닿을 수 없는 오브젝트" 이지만 역은 성립하지 않음.
다음 예제를 통해 알고리즘이 동작하는 모습을 알아보겠습니다.
function marry(man, woman) {
woman.husband = man;
man.wife = woman;
return {
father: man,
mother: woman
}
}
let family = marry({
name: "John"
}, {
name: "Ann"
});
roots로 부터 family가 참조되어 있고, 그 안에서 father와 mother로 각 John과 Ann 객체가 서로를 참조하고 있는 메모리 구조를 볼 수 있습니다. 그럼 이제 delete 구문을 통해 참조하는 속성을 제거해보겠습니다.
delete family.father;
delete family.mother.husband;
console.log(family)
// { mother: { name: 'Ann' } }
(* delete 연산자는 객체의 속성을 제거하는 것으로 제거한 객체의 참조가 사용되지 않으면 나중에 자원을 회수.)
John 객체에 대한 두 참조가 모두 제거되었기 때문에 가비지 컬렉션에 의해 John 객체는 사라지고 Ann 만 남는 모습을 볼 수 있습니다.
단계별 수행
- 가비지 컬렉터는 루트(root) 정보를 수집하고 이를 기억(mark) 합니다.
- 루트가 참조하고 있는 모든 객체를 방문하고 이것들을 기억(mark)합니다.
- mark 된 모든 객체에 방문하고 그 객체들이 참조하는 객체도 mark 합니다. 한번 방문한 객체는 전부 mark하기 때문에 같은 객체를 다시 방문하는 일은 없습니다.
- 루트에서 도달 가능한 모든 객체를 방문할 때까지 위 과정을 반복합니다.
- mark 되지 않은 모든 객체를 메모리에서 삭제합니다.
2012년 기준으로 모든 최신 브라우저들은 가비지 콜렉션에서 마크 앤 스윕 알고리즘을 사용합니다. 또한 순환 참조 문제도 더 이상 발생하지 않습니다.
가비지 컬렉션을 더 빠르게 하는 오래 살아남은 객체는 덜 감시하는 세대별 수집, 가비지 컬렉터가 방문하는 것을 여러 부분으로 분리하여 수행하는 점진적 수집, CPU가 유휴 상태일 때만 실행하는 유휴 시간 수집 등의 여러 최적화 기법이 존재됩니다.
한계 : 수동 메모리 해제
어떤 메모리들은 언제 해제할 지에 대해 수동으로 결정하는 것이 편리할 때가 있습니다. 수동으로 객체의 메모리를 해제하려면 객체 메모리에 도달할 수 없도록 명시하는 기능이 있어야 합니다. 하지만 2019년까지 JS에서는 명시적으로 가비지 컬렉션을 작동할 수 없습니다. 즉, 엔진이 자동으로 수행하므로 개발자가 이를 억지로 실행하거나 막을 수 없다는 것입니다.
JS에서의 가비지 컬렉션에 대해서 알아보았습니다. 자동으로 처리되는 가비지 컬렉터 때문에 당황스러울 순 있지만, 어지간한 개발을 하는데 있어서 이점이 더 많은 것으로 생각됩니다. 이에 대해 공부하다 보니 JS의 메모리 구조에 대해서도 많은 생각을 갖게 된다.
[출처 : https://developer.mozilla.org/ko/docs/Web/JavaScript/Memory_Management]
[출처 : https://ko.javascript.info/garbage-collection]
'Programming Language > JavaScript' 카테고리의 다른 글
JavaScript - 이벤트 루프(Event Roop) [런타임 모델] (0) | 2022.07.13 |
---|---|
JavaScript - Promise (프로미스) (0) | 2022.07.11 |
JavaScript - 클로저(Closure) (0) | 2022.07.07 |
JavaScript - 변수(Variable) (0) | 2022.07.07 |
JavaScript - 콜백 함수(Callback) (0) | 2022.07.06 |