JavaScript 메모리 관리(MDN문서 이해)

HyunHo Lee·2022년 1월 13일
0

JavaScript

목록 보기
14/20
post-thumbnail

자바스크립트는 객체가 생성되었을 때 자동으로 메모리를 할당하고 쓸모 없어졌을 때 자동으로 해제한다(가비지 컬렉션). 이러한 자동 메모리 관리는 잠재적 혼란의 원인이기도 한데, 개발자가 메모리 관리에 대해 고민할 필요가 없다는 잘못된 인상을 줄 수 있기 때문이다.

MDN 문서에서는 자바스크립트의 메모리 관리에 대해 위와 같이 설명하고 있다. 조금 더 자세하게 알아보자.


할당

var num = 123;
var str = 'abc'; 

자바스크립트는 값을 선언할 때 자동으로 메모리를 할당한다. num은 123이라는 정수를 위한 메모리를, str은 abc라는 string을 위한 메모리를 할당하고 있다.

var o = {
  a: 1,
  b: null
};

var a = [1, null, 'abra'];

object의 경우 object와 object에 포함된 값들을 위한 메모리를 할당한다. (배열도 마찬가지..)

function f(a) {
  return a + 2;
}

someElement.addEventListener('click', function(){
  someElement.style.backgroundColor = 'blue';
}, false);

호출 가능한 object인 함수를 위한 메모리를 할당한다.
함수식 또한 object를 담기위한 메모리를 할당한다.

var date = new Date();

var elem = document.createElement('div');

함수 호출의 결과 메모리 할당이 일어나기도 한다. 변수 date는 Date 개체를 위해 메모리를 할당하고, elem는 DOM 엘리먼트를 위해 메모리를 할당한다.

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

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

메소드가 새로운 값이나 오브젝트를 할당하기도 한다.


메모리 할당 후..

할당된 메모리를 읽고 쓰는 값 사용이 있다. 변수나 객체 속성의 값을 읽고 쓰거나 함수 호출시 함수에 인수를 전달하여 수행 할 수 있다.

값 사용을 하지 않는 메모리. 즉, 할당된 메모리가 더 이상 사용되지 않는 경우 해제해야한다. 그렇지 않으면 메모리 누수가 발생할 것이다.

메모리 누수란 부주의한 코딩 또는 오류로 인해 더 사용되지 않는 메모리를 해제하지 못하는 것이다. 사용하지 않는 메모리가 공간을 차지하고 있다면 매우 비효율적인 프로젝트가 될 것이다.

어떤 메모리가 사용되고 있지 않은지를 판단할까? 저 수준의 언어에서는 개발자가 직접 메모리를 해제하지만 고 수준의 언어인 자바스크립트는 가비지 콜렉션을 이용한다.

스택 메모리와 힙 메모리

원시 타입인 단순 변수 string, number, boolean, null, undefined, symbol 등은 스택 메모리를 사용한다.

참조 타입인 object, array, function 등은 힙 메모리를 사용한다.


가비지 콜렉션

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


Reference-counting 알고리즘

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


var y = x;      // 'y' 변수는 위의 오브젝트를 참조하는 두 번째 변수이다.

x = 1;          // 이제 'y' 변수가 위의 오브젝트를 참조하는 유일한 변수가 되었다.

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

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

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

더 이상 필요없는 object를 어떤 다른 object도 참조하지 않는 object라고 정의한다. 이 오브젝트를 가비지라 부르며, 이를 참조하는 다른 오브젝트가 하나도 없는 경우, 수집이 가능하다.


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

  return "azerty";
}

f();

위의 예제에서는 두 객체가 서로 참조하는 속성으로 생성되어 순환 구조를 생성한다. 함수 호출이 완료되면 이 두 객체는 스코프를 벗어나게 될 것이며, 그 시점에서 두 객체는 불필요해지므로 할당된 메모리는 회수되어야 한다. 그러나 두 객체가 서로를 참조하고 있으므로, Reference-counting 알고리즘은 둘 다 가비지 컬렉션의 대상으로 표시하지 않는다. 메모리 누수의 원인이다.

var div;
window.onload = function() {
  div = document.getElementById('myDivElement');
  div.circularReference = div;
  div.lotsOfData = new Array(10000).join('*');
};

DOM 요소 "myDivElement"는 "circularReference" 속성에서 자신에 대해 순환 참조하고 있다. 속성이 명시적으로 제거되거나 null이 되지않으면 Reference-counting 가비지 수집기는 항상 하나 이상의 참조를 그대로 유지하며 DOM 트리에서 제거 된 경우에도 DOM 요소를 메모리에 유지합니다. DOM 요소가 많은 양의 데이터를 보유하는 경우 (위의 예에서 'lotsOfData'속성으로 설명 됨)이 데이터에 사용 된 메모리는 절대 해제되지 않으며 브라우저가 점점 더 느려지는 등 메모리 관련 문제로 이어질 수 있다.


Mark-and-sweep 알고리즘

이 알고리즘은 "더 이상 필요없는 오브젝트"를 "닿을 수 없는 오브젝트"로 정의한다.

이 알고리즘은 roots 라는 오브젝트의 집합을 가지고 있다(자바스크립트에서는 전역 변수들을 의미한다). 주기적으로 가비지 콜렉터는 roots로 부터 시작하여 roots가 참조하는 오브젝트들, roots가 참조하는 오브젝트가 참조하는 오브젝트들... 을 닿을 수 있는 오브젝트라고 표시한다. 그리고 닿을 수 있는 오브젝트가 아닌 닿을 수 없는 오브젝트에 대해 가비지 콜렉션을 수행한다.

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

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

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


마무리

요약

저수준 언어에서는 메모리를 관리하기 위해 개발자가 직접 해제해야 한다. 고수준의 언어인 자바스크립트는 객체가 생성되었을 때 자동으로 메모리를 할당하고 필요 없게 되면 자동으로 해제하는 가비지 콜렉션이 있다. 처음에는 필요 없는 메모리를 판단하는 기준을 Reference-counting 알고리즘로 결정했다. 이 알고리즘은 어떤 다른 오브젝트도 참조하지 않는 오브젝트를 필요없는 오브젝트로 정의한다. 하지만 순환 참조를 다루는 부분에서 불필요한 오브젝트가 가비지 콜렉션으로 분리되지 않는 한계점이 있어 메모리 누수가 발생했다. 그래서 나온 알고리즘이 Mark-and-sweep 알고리즘이다. 전역 변수로부터 시작하여 전역 변수가 참조하는 오브젝트, 그 오브젝트가 참조하는 오브젝트 등을 닿을 수 있는 오브젝트라고 정의하고 그 반대를 닿을 수 없는 오브젝트라고 한다. 닿을 수 없는 오브젝트는 가비지 콜렉션이 되고, 이 알고리즘은 순환 참조에서의 문제점을 해결했다.

개념 정리 후 보면 좋은 글

profile
함께 일하고 싶은 개발자가 되기 위해 달려나가고 있습니다.

0개의 댓글