목록

js 메모리 생존주기 탐구

가비지컬렉션 이란..

할당된 메모리가 더 이상 필요없을 떄 해제해 주지만, 일반적인 경우에 어떤 메모리가 필요없는지 알아내는 것은 알고리즘으로 풀 수 없는 비결정적인 문제이기 떄문에. 안전하지만 완벽하진 않다? 따라서 프로그래머가 직접 관리하며 신경써줘야 한다..

참조-세기(Reference-counting) 가비지 콜렉션

가장 무난한 알고리즘이다.

이 알고리즘은 “더 이상 필요없는 오브젝트”를 “어떤 다른 오브젝트도 참조하지 않는 오브젝트” 라고 정의한다.

참조하는 오브젝트가 하나도 없다면 가비지 콜렉션이 수행한다.

일반적으로, 변수가 다른곳에 참조 않해야지 가비지 컬렉션이 수행하는데 주의할 부분은,

해당 변수의 속성값이 하나라도 다른곳에 참조 하고 있다면 참조대상이므로 가비지 컬렉션이 수집하지 않는다.

C++ 공부 할 떄 메모리 해제 시 해당 변수 참조 시 카운팅을 해서 0인 경우 에만 해제 되도록 만들었던 기억이 있다.
(C++에서는 함부로 막 해제하면 안된다, 다른 곳에서 참조하고 있는데 해제해버리면 댕글리포인터 되버리므로..)

—예제

var o = {
  a: {
    b:2
  }
}; // 2개의 오브젝트가 생성되었다. 하나의 오브젝트는 다른 오브젝트의 속성으로 참조된다.
// 나머지 하나는 'o' 변수에 할당되었다.
// 명백하게 가비지 콜렉션 수행될 메모리는 하나도 없다.


var o2 = o; // 'o2' 변수는 위의 오브젝트를 참조하는 두 번째 변수이다.
o = 1; // 이제 'o2' 변수가 위의 오브젝트를 참조하는 유일한 변수가 되었다.

var oa = o2.a; // 위의 오브젝트의 'a' 속성을 참조했다.
// 이제 'o2.a'는 두 개의 참조를 가진다. 'o2'가 속성으로 참조하고 'oa'라는 변수가 참조한다.

o2 = "yo"; // 이제 맨 처음 'o' 변수가 참조했던 오브젝트를 참조하는 오브젝트는 없다(역자: 참조하는 유일한 변수였던 o2에 다른 값을 대입했다)
// 이제 오브젝트에 가비지 콜렉션이 수행될 수 있을까?
// 아니다. 오브젝트의 'a' 속성이 여전히 'oa' 변수에 의해 참조되므로 메모리를 해제할 수 없다.

oa = null; // 'oa' 변수에 다른 값을 할당했다. 이제 맨 처음 'o' 변수가 참조했던 오브젝트를 참조하는 다른 변수는 없으므로 가비지 콜렉션이 수행된다.

—한계 : 순환

두 오브젝트가 서로를 참조하면 문제가 발생한다.

두 오브젝트 모두 필요 없어졌더라도 가비지 콜렉션을 수행할 수 없다.

function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o는 o2를 참조한다.
  o2.a = o; // o2는 o를 참조한다.

  return "azerty";
}

f();
// 두 오브젝트가 만들어지고 서로를 참조해서 순환이 일어났다.
// 함수가 종료되고 나면 사실상 두 오브젝트는 의미가 없어지므로 가비지 콜렉션이 수행되어야 한다.
// 그러나 위의 참조-세기 알고리즘에서는 두 오브젝트 모두 참조를 가지고 있기 때문에 둘 다 가비지 콜렉션이 일어나지 않는다.

실제로 익스플로러 6, 7 은 DOM 오브젝트에 대해 참조-세기 알고리즘으로 가비지 콜렉션을 수행한다.

흔히, 이 두 브라우저에서는 다음과 같은 패턴의 메모리 누수가 발생한다고 한다.

var div = document.createElement("div");
div.onclick = function(){
  doSomething();
}; // div 오브젝트는 이벤트 핸들러를 'onclick' 속성을 통해 참조한다.
// 이벤트 핸들러의 스코프에도 div 오브젝트가 있으므로 div 오브젝트에 접근할 수 있다. 따라서 이벤트 핸들러도 div 오브젝트를 참조한다.
// 순환이 발생했고 메모리 누수가 일어난다.

표시하고-쓸기(Mark-and-sweep) 알고리즘

이것은 “더 이상 필요없는 오브젝트”를 “닿을 수 없는 오브젝트” 로 정의한다.

이 알고리즘은 roots 라는 오브젝트의 집합을 가지고 있다(자바스크립트에서는 전역 변수들을 의미한다).

주기적으로 가비지 콜렉터는 roots로 부터 시작하여 roots가 참조하는 오브젝트들, roots가 참조하는 오브젝트가 참조하는 오브젝트들… 을 닿을 수 있는 오브젝트라고 표시한다.

그리고 닿을 수 있는 오브젝트가 아닌 닿을 수 없는 오브젝트에 대해 가비지 콜렉션을 수행한다.
(DFS깊이 우선 탐색 알고리즘 느낌???)

참조-세기 알고리즘보다 효율적이라고 한다.

이유는 “참조되지 않는 오브젝트”는 모두 “닿을 수 없는 오브젝트” 이지만(아까 정의에 의해) 역은 성립하지 않기떄문!!(나만 이해 안되나..)

최신 브라우저들은 모두 이 알고리즘 가비지 콜렉션을 사용한다고 합니다.

이 알고리즘으로는 위에 한계를 해결할 수 있는데,

첫 번째 예제에서 함수가 리턴되고 나서 두 오브젝트는 닿을 수 없다.

따라서 가비지 콜렉션이 일어난다.

두 번쨰도 마찬가지. div변수와 이벤트 핸들러가 roots로 닿을 수 없어지면 참조가 일어남에도 불구하고 가비지 콜렉션이 일어남.

—한계 : 오브젝트들은 명시적으로 닿을 수 없어져야 한다.

이 한계가 지적되었지만 실제로는 사람들은 이 문제를 비롯한 가비지 콜렉션에 별 관심이 없다고 한다.

결론

  • 사용하지 않은 객체, 변수는 모두 null로 초기화.
  • 이벤트 핸들러를 바인딩 했다면, 모두 언바인딩
  • DOM을 동적으로 생성했따면, 불필요한 객체, 속성(값)을 DOM에 삽입하지 말자

참고 : MDN Javascript 부분