가비지 컬렉션과 동작 방식
앞 전 포스팅에서 가비지와,가비지 컬렉터, 가비지 컬렉션에 대해 알아보았고, 마지막에 가비지 컬렉션 작동 방식에 대해 설명하던 중, 알고리즘 한계점의 발견, 즉 메모리 누수(Memory Leak)에 대해 잠깐 언급하였다.
오늘은 메모리 누수는 왜 발생이 되는 것인지 알아보기로 하자.
우리는 코딩하면서 한번 이상 전역 변수의 사용을 지양하라는 소리를 들어본적이 있을것이다. 왜일까? 단순한 버그의 문제 때문에?
버그의 문제도 맞지만 가장 중요한건, 무분별한 글로벌 변수 사용시 원치않는 메모리 누수로 빠지게 때문이다.
window->global Object
node->global Object
보통 브라우저에서는 window라는 global Object가 있고, node에서는 golbal이라는 global Object가 있다.
function foo(arg) {
bar = "Hello World";
}
위 코드에서 보면 함수 foo안에 bar라는 변수에 "Hello World"라는 string을 할당 받는 것을 볼 수 있다.
여기서 특징은 bar라는 변수를 선언할때 어떠한 키워드도 사용하고 있지 않다는 점이다.
그럼 이렇게 되면 어떠한 현상이 발생될까?
이렇게 키워드를 사용하지않고 변수를 선언하게 되면 자동으로
window라는global Object의property로 설정이 된다.
이렇게 됐을때 문제점은, foo라는 함수만 bar라는 변수를 참조하는 것이 아니라, 글로벌에 있는 함수에서 bar라는 변수를 참조하게 된다.
원래 내부의 bar라는 변수가foo라는 함수의 지역 변수로 할당이되고 나서, foo가 실행이되고 나서 종료가 되면, bar는 변수로써 남아있지 않고 release되게 되는데, window의 property로 등록이 되어 버리면 메모리가 release되지 않게 되어 버린다.
즉,사용되어지지 않음에도 불구하고, 가비지컬렉터가 메모리를 수거해야하는데 수거를 못해간다는 뜻이다.
또한, this를 이용해서도 뜻하지 않은 전역 변수를 생성 할 수 있다.
function foo() {
this.var1 = "글로벌 변수 입니다.";
}
// 다른 함수 내에 있지 않은 foo를 호출하면 this는 글로벌 객체(window)를 가리킴
foo();
일반적인 함수내의 this 키워드는 바로 window를 가르킨다.
따라서,
this.var1 === window.var1과 동일하다고 볼 수 있다.
이렇게 되면 위에서 보았던 똑같은 문제를 야기될 수 있다.
그렇기에 함수안에서 변수를 선언할때는 꼭 앞에 키워드인 const,let을 작성해줘야 메모리 누수를 방지할 수 있다.(var는 이제 안쓰임으로 제외)
잊혀진 타이머와 콜백? 이게 무슨 뜻이지? 라고 생각하는 사람들이 있을것이다.
var serverData = loadData();
setInterval(function() {
var renderer = document.getElementById('renderer');
if(renderer) {
renderer.innerHTML = JSON.stringify(serverData);
}
}, 5000); // 매 5초 마다 실행
위 코드를 보고 한가지 가정을 해보자.
지금 DOM에서 가져온 renderer라는 객체가 바뀔수가 있고, 제거될 수 있다.
이럴 경우에는 더이상 renderer안에 그려지게 될 serverData는 더이상 바뀌거나 제거된 renderer안에는 더이상 참조될 일이 없어진다.
그렇지만 우리가 setInterval로 핸들러가 5초마다 실행이 되도록 설정을 해놓았기 때문에 serverData의 참조는 없어지지 않게 되버린다.
Observer의 경우, 더 이상 필요하지 않은 경우 (또는 관련 객체에 접근하지 못하게 하려는 경우) 해당객체를 제거하기 위해 명시적으로 호출하는 것이 중요하다.
과거 특정 브라우저 (IE6)가 순환 참조를 잘 관리하지 못했기 때문에 이 부분은 특히 중요하다. (자세한 내용은 아래 참조)
하지만 현대의 대부분의 브라우저는 observe 객체가 더 이상 참조되지 않는다면, 리스너가 명시적으로 제거되지 않는다 하더라도 메모리를 해제 한다.
객체를 없애기전에 이러한 observer를 명시적으로 제거하는 것은 좋은 관례다.
아래 예가 좋은 예시이다.
var element = document.getElementById('launch-button');
var counter = 0;
function onClick(event) {
counter++;
element.innerHtml = 'text';
}
element.addEventListener('click', onClick);
// 필요한 작업 수행
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
// 이제 `element` 참조를 없앰으로서, 가비지 컬렉터가 가비지로 인식하여 수거해간다.
// `element`와 `onClick`모두 스코프에서 사라진다.
// 오래된 브라우저에서는 이러한 순환참조를 잘 해결하지 못했다.
클로저에 대해서는 이야기할 부분이 너무 많아 따로 정리를 해놓았고, 자세한 부분은 그 내용을 참고해보자.
Javascript Scope(유효범위),Closure(클로저)
그래도 간단하게 짚고 넘어가자면
클로저는
InnerFunction이다.
클로져는 자신을 감싸고 있는 바깥 함수의 변수에 접근할 수 있는 내부의 함수를 말한다.
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing) // 'originalThing'에 대한 참조
console.log("hi");
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log("message");
}
};
};
setInterval(replaceThing, 1000);
위 코드의 동작 순서를 보도록 하자.
replaceThing이 호출 되면 theThing은 커다란 배열과 새로운 클로져(someMethod)를 포함하는 새로운 객체를 얻게된다. originalThing은 unused 변수가 갖고 있는 클로져에 의해 참조되고 있다.(which is theThing variable from the previous call to replaceThing).someMethod 클로져를 위해 생성된 스코프는 unused와 공유되었다. unused는 originalThing에 대한 참조를 갖고 있다. unused가 다시 사용되지 않는다 해도 someMethod는 theThing을 통해 replaceThing의 스코프 바깥에서 글로벌하게 사용될 수 있다.someMethod는 unused와 클로져 스코프를 공유하기 때문에 unused가 originalThing에 대해 갖고 있는 참조 때문에 강제로 활성 상태가 유지된다.위 코드에서 someMethod 클로저에 의해 생성된 스코프는 unused와 공유되고, unused는 originalThing을 참조하게 된다.
unused가 사용된적이 없어도 someMethod는 replaceThing 스코프의 바깥에서 theThing을 통해 사용될 수 있다.
unused가 originalThing을 참조한다는 사실 때문에 unused는 활성 상태가 유지되는데 왜냐하면 someMethod가 unused와 클로져 스코프를 공유하기 때문이다.
즉, 이와 같은 현상처럼 unused은 사용되어지지도 않으면서, release가 되지않는 메모리이고, 이것은 메모리 누수로 이어진다.
이 경우는 바로 코드를 보면서 설명하기로 한다.
var elements = {
button: document.getElementById('button'),
image: document.getElementById('image')
};
function doStuff() {
elements.image.src = 'http://example.com/image_name.png';
}
function removeImage() {
// image는 body 요소의 바로 아래 자식임
document.body.removeChild(document.getElementById('image'));
// image는 지웠지만, element의 button 요소 객체에 대한 참조가 아직 존재하여, image 요소는 아직도 메모리 상에 있고 가비지컬렉터가 수거할 수 없게된다.
}
여기서 중요한 점은 DOM Tree에서 가져온
image요소를element의 프로퍼티로 할당한 다음, 이후image요소를 제거 했다면, 반드시button에서도null값을 할당해줘야 한다.
그렇지 않으면 이 image 요소는 지워졌음에도 불구하고, 여전히 이 element를 참조하게 된다.
추가적으로 고려해야할 사항은 DOM 트리에서 내부 혹은 말단 노드에 대한 참조다.
<td/>)에 대한 참조를 가지고 있다고 한다. table을 제거하기로 했지만, 여전히 셀에 대한 참조를 가지고 있게 된다. table의 자식노드고, 자식 노드는 부모 노드의 참조를 유지한다. table의 셀에 대한 참조로 인해 테이블 전체가 메모리에 유지 된다. DOM의 요소에 대해 참조할 때는 이점을 유의해야 한다.