가비지 컬렉션과 동작 방식
앞 전 포스팅에서 가비지와,가비지 컬렉터, 가비지 컬렉션에 대해 알아보았고, 마지막에 가비지 컬렉션 작동 방식에 대해 설명하던 중, 알고리즘 한계점의 발견, 즉 메모리 누수(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의 요소에 대해 참조할 때는 이점을 유의해야 한다.