그저 내가 되었고

⚡️JS:: 메모리 관리(가비지 컬렉션) 본문

개발/JavaScript

⚡️JS:: 메모리 관리(가비지 컬렉션)

hyuunii 2022. 12. 12. 11:33

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

C 언어같은 저수준 언어에서는 메모리 관리를 위해 malloc() 과 free()를 사용합니다. 반면, 자바스크립트는 객체가 생성되었을 때 자동으로 메모리를 할당하고 더 이상 필요하지 않을 때 자동으로 해제합니다(가비지 컬렉션). 이러한 자동 메모리 관리는 잠재적 혼란의 원인이기도 한데, 개발자가 메모리 관리에 대해 고민할 필요가 없다는 잘못된 인상을 줄 수 있기 때문입니다.

 

메모리 생존주기

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

  1. 필요할 때 할당합니다.
  2. 할당된 메모리를 사용합니다. (읽기, 쓰기)
  3. 더 이상 필요하지 않으면 해제합니다.

두 번째 부분은 모든 언어에서 명시적으로 사용됩니다. 그러나 첫 번째 부분과 마지막 부분은 저수준 언어에서는 명시적이며, 자바스크립트와 같은 대부분의 고수준 언어에서는 암묵적으로 작동합니다.

 

JS의 메모리 할당

값 초기화

프로그래머를 할당 문제로 괴롭히지 않기 위해서, 자바스크립트는 값을 선언할 때 자동으로 메모리를 할당합니다. 

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(); // 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);
// a 와 a2 를 이어붙여, 4개의 원소를 가진 새로운 배열

 

값 사용

값 사용이란 기본적으로는 할당된 메모리를 읽고 쓰는 것을 의미합니다. 변수나 객체 속성의 값을 읽고 쓰거나 함수 호출 시 함수에 인수를 전달하여 수행 할 수 있습니다.

 

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

이 단계에서 대부분의 문제가 발생합니다. "할당된 메모리가 더 이상 필요없을 때"를 알아내기가 어렵기 때문입니다.

저수준 언어에서는 메모리가 필요없어질 때를 개발자가 직접 결정하고 해제하는 방식을 사용합니다.

자바스크립트와 같은 고수준 언어들은 "가비지 콜렉션(GC)"이라는 자동 메모리 관리 방법을 사용합니다. 가비지 콜렉터의 목적은 메모리 할당을 추적하고 할당된 메모리 블록이 더 이상 필요하지 않게 되었는지를 판단하여 회수하는 것입니다. 이러한 자동 메모리 관리 프로세스가 궁극의 방법은 아닙니다. 왜냐하면 어떤 메모리가 여전히 필요한지 아닌지를 판단하는 것은 비결정적 문제이기 때문입니다.

* 비결정적 문제(Decidability (logic))

In logic, a true/false decision problem is decidable if there exists an effective method for deriving the correct answer. Zeroth-order logic (propositional logic) is decidable, whereas first-order and higher-order logic are not. Logical systems are decidable if membership in their set of logically valid formulas (or theorems) can be effectively determined. A theory (set of sentences closed under logical consequence) in a fixed logical system is decidable if there is an effective method for determining whether arbitrary formulas are included in the theory. Many important problems are undecidable, that is, it has been proven that no effective method for determining membership (returning a correct answer after finite, though possibly very long, time in all cases) can exist for them.

 

가비지 컬렉션

위에서 언급한 것처럼 "더 이상 필요하지 않은" 모든 메모리를 찾는건 비결정적 문제입니다. 따라서 가비지 컬렉터들은 이 문제에 대한 제한적인 해결책을 구현합니다. 이 섹션에서는 주요한 가비지 컬렉션 알고리즘들과 그 한계를 이해하는데 필요한 개념을 설명합니다.

참조

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

이 컨텍스트에서 "오브젝트"의 개념은 일반 JavaScript 오브젝트와 함수 범위(또는 정적 어휘 범위)를 포함하여 더 넓게 확장됩니다.

 

 

가비지 컬렉션 기준

자바스크립트는 도달 가능성(reachability) 이라는 개념을 사용해 메모리 관리를 수행합니다.

‘도달 가능한(reachable)’ 값은 쉽게 말해 어떻게든 접근하거나 사용할 수 있는 값을 의미합니다. 도달 가능한 값은 메모리에서 삭제되지 않습니다.

  1. 아래 소개해 드릴값들은 그 태생부터 도달 가능하기 때문에, 명백한 이유 없이는 삭제되지 않습니다.
    • 현재 함수의 지역 변수와 매개변수
    • 중첩 함수의 체인에 있는 함수에서 사용되는 변수와 매개변수
    • 전역 변수
    • 기타 등등
    이런 값은 루트(root) 라고 부릅니다.
  2. 예시:
  3. 루트가 참조하는 값이나 체이닝으로 루트에서 참조할 수 있는 값은 도달 가능한 값이 됩니다.
  4. 전역 변수에 객체가 저장되어있다고 가정해 봅시다. 이 객체의 프로퍼티가 또 다른 객체를 참조하고 있다면, 프로퍼티가 참조하는 객체는 도달 가능한 값이 됩니다. 이 객체가 참조하는 다른 모든 것들도 도달 가능하다고 여겨집니다. 자세한 예시는 아래에서 살펴보겠습니다.

자바스크립트 엔진 내에선 가비지 컬렉터(garbage collector)가 끊임없이 동작합니다. 가비지 컬렉터는 모든 객체를 모니터링하고, 도달할 수 없는 객체는 삭제합니다.

 

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' 변수가 참조했던 오브젝트를 참조하는
                // 다른 변수는 없으므로 가비지 콜렉션이 수행됩니다.

 

한계: 순환 참조

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

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

  return "azerty";
}

f();

 

Mark-and-sweep 알고리즘

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

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

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

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

'가비지 컬렉션’은 대개 다음 단계를 거쳐 수행됩니다.

  • 가비지 컬렉터는 루트(root) 정보를 수집하고 이를 ‘mark(기억)’ 합니다.
  • 루트가 참조하고 있는 모든 객체를 방문하고 이것들을 ‘mark’ 합니다.
  • mark 된 모든 객체에 방문하고 그 객체들이 참조하는 객체도 mark 합니다. 한번 방문한 객체는 전부 mark 하기 때문에 같은 객체를 다시 방문하는 일은 없습니다.
  • 루트에서 도달 가능한 모든 객체를 방문할 때까지 위 과정을 반복합니다.
  • mark 되지 않은 모든 객체를 메모리에서 삭제합니다.

루트에서 페인트를 들이붓는다고 상상하면 이 과정을 이해하기 쉽습니다. 루트를 시작으로 참조를 따라가면서 도달가능한 객체 모두에 페인트가 칠해진다고 생각하면 됩니다. 이때 페인트가 묻지 않은 객체는 메모리에서 삭제됩니다.

 

 

한계: 수동 메모리 해제.

어떤 메모리를 언제 해제할지에 대해 수동으로 결정하는 것이 편리할 때가 있습니다. 그리고 수동으로 객체의 메모리를 해제하려면, 객체 메모리에 도달할 수 없도록 명시하는 기능이 있어야 합니다.

2019년 현재의 JavaScript에서는 명시적으로 또는 프로그래밍 방식으로 가비지 컬렉션을 작동할 수 없습니다.

 

REFERENCE
https://ko.javascript.info/garbage-collection
https://developer.mozilla.org/ko/docs/Web/JavaScript/Memory_Management