관리 메뉴

HAMA 블로그

자바스크립트의 메모리관리 본문

Javascript

자바스크립트의 메모리관리

[하마] 이승현 (wowlsh93@gmail.com) 2015. 5. 24. 09:37

https://developer.mozilla.org/ko/docs/Web/JavaScript/Memory_Management  펌


개요

C 언어같은 저급 언어는 메모리 관리를 위해 malloc() 과 free()를 사용한다. 반면, 자바스크립트는 무언가가 생성되었을 때(오브젝트나 문자열 등) 메모리를 할당하고 쓸모 없어졌을 때 '자동으로' free 한다. '자동으로' 라는 말에는 혼란의 여지가 있다. 이는 자바스크립트를 포함한 여러 고급 언어 개발자들에게 메모리 관리가 불가능하다는 인상을 준다. 하지만 실상은 그렇지 않다.  

메모리 생존주기

메모리 생존주기는 프로그래밍 언어와 관계없이 비슷하다.

  1. 필요할때 할당한다.
  2. 사용한다. (읽기, 쓰기)
  3. 필요없어지면 해제한다. 

첫 번째 부분과 두 번째 부분은 모든 언어에서 분명하게 기술되지만 마지막 부분은 조금 다르다. 저급 언어에서는 분명히 기술되지만 자바스크립트 같은 고급 언어에서는 분명하게 기술되지 않는다(역자: 명시적으로 free를 하지 않는다는 의미). 

자바스크립트에서 메모리 할당

값 초기화

자바스크립트에서는 프로그래머들이 일일히 메모리 할당을 하는 수고를 덜어주기위해 값을 선언할 때 메모리를 할당한다. 

var n = 123; // 정수를 담기 위한 메모리 할당
var s = "azerty"; // 문자열을 담기 위한 메모리 할당

var o = {
  a: 1,
  b: null
}; // 오브젝트와 그 오브젝트에 포함된 값들을 담기 위한 메모리 할당

var a = [1, null, "abra"]; // (오브젝트 처럼) 배열과 배열에 담긴 값들을 위한 메모리 할당

function f(a){
  return a + 2;
} // 함수를 위한 할당(함수는 '호출가능한' 오브젝트이다)

// 함수식 또한 오브젝트를 담기위한 메모리를 할당한다. 
someElement.addEventListener('click', function(){
  someElement.style.backgroundColor = 'blue';
}, false);

함수 호출을 통한 할당

몇 가지 함수에서도 메모리 할당이 일어난다. 

var d = new Date();
var e = document.createElement('div'); // DOM 엘리먼트를 위해 메모리를 할당한다.

몇 가지 메쏘드도 새로운 값이나 오브젝트를 담기 위해 메모리 할당이 일어난다.

var s = "azerty";
var s2 = s.substr(0, 3); // s2는 새로운 문자열
// 자바스크립트에서 문자열은 immutable 값이기 때문에 메모리를 새로 할당하지 않고 단순히 [0, 3] 이라는 범위만 저장한다. 

var a = ["ouais ouais", "nan nan"];
var a2 = ["generation", "nan nan"];
var a3 = a.concat(a2); // 4개의 원소를 가진 새로운 배열

값 사용

값 사용이란 기본적으로는 할당된 메모리를 읽고 쓰는 것을 의미한다. 변수나 오브젝트 속성 값을 읽고 쓸때 값 사용이 일어난다. 또 함수 호출시 함수에 인수를 넘길때도 일어난다. 

할당된 메모리가 더 이상 필요없을 때 해제하기

이 단계에서 대부분의 문제가 발생한다. "할당된 메모리가 더 이상 필요없을 때"를 알아내기가 힘들기 때문이다. 이제까지는 개발자들이 메모리가 필요없어질 때를 정하고 free하곤 했다. 

고급 언어 인터프리터는 "가비지 콜렉터"라는 소프트웨어를 가지고 있다. 가비지 콜렉터란 메모리 할당을 추적하고 할당된 메모리가 더 이상 필요 없어졌을 때 해제하는 작업을 한다. 이 작업은 근사적인 작업이다. 왜냐하면 일반적인 경우에 어떤 메모리가 필요없는지 알아내는 것은 알고리즘으로 풀 수 없는 비결정적인 문제이기 때문이다. (역자: 세상에 존재하는 모든 가비지 콜렉터는 안전하지만 완전하지 않다. 가비지 콜렉터는 항상 필요없어진 메모리만을 해제하지만 모든 필요없어진 메모리를 해제하는건 아니다)

가비지 콜렉션

위에서 언급한 것처럼 "더 이상 필요없는" 모든 메모리를 찾는건 비결정적이다. 따라서 몇 가지 제한을 두어 "더 이상 필요없는 모든 메모리"가 아니라 "더 이상 필요없는 몇몇 메모리"를 찾아보자. 몇 개의 가비지 콜렉션 알고리즘을 소개하고 한계점을 알아볼 것이다.

참조

가비지 콜렉션 알고리즘의 핵심 개념은 참조이다. A라는 메모리를 통해 (명시적이든 암시적이든) B라는 메모리에 접근할 수 있다면 "B는 A에 참조된다" 라고 한다. 예를 들어 모든 자바스크립트 오브젝트는 prototype 을 암시적으로 참조하고 그 오브젝트의 속성을 명시적으로 참조한다.

앞으로 "오브젝트"라는 어휘의 의미를 넓혀서 기존의 자바스크립트 오브젝트뿐만 아니라 함수 스코프도 포괄하자.

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

참조-세기 알고리즘은 가장 무난한 알고리즘이다. 이 알고리즘은 "더 이상 필요없는 오브젝트"를 "어떤 다른 오브젝트도 참조하지 않는 오브젝트"라고 정의한다. 어떤 오브젝트를 참조하는 다른 오브젝트가 하나도 없다면 그 오브젝트에 대해 가비지 콜렉션을 수행한다.

예제

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가 참조하는 오브젝트가 참조하는 오브젝트들... 을 닿을 수 있는 오브젝트라고 표시한다. 그리고 닿을 수 있는 오브젝트가 아닌 닿을 수 없는 오브젝트에 대해 가비지 콜렉션을 수행한다.

이 알고리즘은 위에서 설명한 참조-세기 알고리즘보다 효율적이다. 왜냐하면 "참조되지 않는 오브젝트"는 모두 "닿을 수 없는 오브젝트" 이지만 역은 성립하지 않기 때문이다. 위에서 반례인 순환 참조하는 오브젝트들을 설명했다.

2012년 기준으로 모든 최신 브라우저들은 가비지 콜렉션에서 표시하고-쓸기 알고리즘을 사용한다. 지난 몇 년간 연구된 자바스크립트 가비지 콜렉션 알고리즘의 개선들은 모두 이 알고리즘에 대한 것이다. 개선된 알고리즘도 여전히 "더 이상 필요없는 오브젝트"를 "닿을 수 없는 오브젝트"로 정의하고 있다.

순환 참조는 이제 문제가 되지 않는다.

첫 번째 예제에서 함수가 리턴되고 나서 두 오브젝트는 닿을 수 없다. 따라서 가비지 콜렉션이 일어난다.

두 번째 예제에서도 마찬가지다. div 변수와 이벤트 핸들러가 roots로 부터 닿을 수 없어지면 순환 참조가 일어났음에도 불구하고 가비지 콜렉션이 일어난다.

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

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

더 보기


Comments