에브리 저장소

[JavaScript] 메모리 관리와 가비지 컬렉션 동작 방식 본문

Front-end/JavaScript

[JavaScript] 메모리 관리와 가비지 컬렉션 동작 방식

eblee 2019. 11. 14. 14:32

 

최근 면접 스터디를 진행하며 받은 질문 중, 클로저를 사용할 때 주의해야 할 점에 대해 질문을 받았으나

선뜻 대답하지 못했습니다. 😢

 

이에 메모리 누수를 주의해야 한다.라는 피드백을 받고 알아보니 클로저인 내부 함수가 참조하는 외부 함수의 객체들을 더 이상 사용하지 않아도 가비지 콜렉터가 수집하지 못한다는 내용이 있었습니다.

 

키워드만 정리해놓고 잊고 있었는데, 다른 분께서 자바스크립트의 가비지 콜렉터 동작 방식에 대해 내용이 있으면 좋을 것 같다는 피드백을 주셨고, 이를 계기로 정리해서 공유합니다.

 

 

 

 

틀린 내용 지적 등의 피드백은 언제나 감사히 받겠습니다.

 

 

참고

 


메모리 생명 주기

 

  1. 필요한 메모리 할당
  2. 할당된 메모리 사용(읽기, 쓰기)
  3. 해당 메모리가 필요 없어지면 해제

프로그래밍 언어에 상관없이 메모리 생명 주기는 위 과정으로, 항상 동일합니다.
하지만 1, 3번을 시행함에 있어 로우 레벨 언어하이 레벨 언어에서 차이가 있습니다.
자바스크립트는 하이 레벨 언어에 속합니다.

 

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

값 초기화

 

프로그래머가 일일이 메모리 할당을 하지 않도록 하기 위해, 자바스크립트는 값을 초기화 할 때 자동으로 메모리를 할당합니다.

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 element 할당

일부 메서드에서도 새로운 값이나 객체를 할당합니다.

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개의 원소를 가진 새로운 배열

 

 

값 사용

 

값을 사용한다는 의미는 할당된 메모리를 쓰거나 읽는다는 것을 의미합니다.
변수나 객체 속성값을 읽고 쓸 때, 함수 호출 시 함수에 인자를 넘길 때 값을 사용하는 것을 의미합니다.

 

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

 

할당된 메모리를 해제하려면 할당된 메모리가 더 이상 필요하지 않은 시기를 결정해야 하는데, 이를 잘못하면 메모리 누수가 발생하게 됩니다. 그러므로 메모리 누수 문제는 할당한 메모리를 적절히 해제하지 못해서 발생한다고 볼 수 있습니다.

 

C, C++과 같은 로우 레벨 언어에서는 개발자가 메모리 해제 시기를 직접 결정해야 합니다. 프로그램에서 할당된 메모리가 더 이상 필요하지 않을 때, 직접 메모리 할당을 해제해야 합니다.

 

자바스크립트와 같은 몇몇 하이 레벨 언어에서는 가비지 컬렉션(GC)이라는 자동 메모리 관리 형식을 활용합니다. 가비지 콜렉터의 목적은 메모리 할당을 모니터링하고 할당된 메모리의 블록이 더 이상 필요하지 않은 시점을 확인하여 회수하는 것입니다.


가비지 콜렉터는 항상 필요 없어진 메모리만을 해제하지만 항상 필요 없어진 모든 메모리를 모두 다 해제한다고 기대할 수 없기 때문에, 안전하지만 완전하지 않다고 생각할 수 있습니다. 이러한 이유로 가비지 컬렉션을 근사적인 작업(approximation)이라고도 합니다.

 

 


가비지 콜렉션

이 파트에서는 두 개의 가비지 컬렉션 알고리즘과 한계점을 알아봅니다.

 

 

참조 (Reference)

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

 

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

이 알고리즘은 "더 이상 필요 없는 객체"를 "어떤 다른 객체도 참조하지 않는 객체"라고 정의합니다. 특정 객체를 참조하는 객체가 하나도 없다면, 그 객체에 대해 가비지 컬렉션을 수행합니다.

 

예제

var o = {
  a: {
    b: 2
  }
};

 

위 코드에는 두 개의 객체가 있습니다. 한 객체는 다른 객체의 속성(a)으로 참조되고 있습니다.
그리고 그를 감싼 객체를 변수(o)에 할당했습니다.
여기서 각 객체는 모두 참조되고 있기 때문에 가비지 컬렉션이 수행될 객체는 없습니다.

이어서 아래 코드를 봅시다.

 

var o2 = o;
o = 1;
// 'o2' 변수는 객체를 참조하는 두 번째 변수입니다.
// 그 후 o에 다른 값을 저장하여, 이제 객체를 참조하는 변수는 o2가 유일합니다.

var oa = o2.a;
// 위의 객체의 'a' 속성을 oa가 참조합니다.
// 이제 'o2.a'는 두 개의 참조를 가진다. 'o2'가 속성으로 참조하고 있고, 'oa'라는 변수가 참조하고 있습니다.

o2 = "yo";
// 참조하는 유일한 변수였던 o2에 다른 값을 대입했고, 
// 이제 맨 처음 'o' 변수가 참조했던 객체를 참조하는 객체는 존재하지 않습니다.

// 하지만 그 객체에 대하여 가비지 콜렉션이 수행 되지 않을 것입니다.
// 객체의 'a' 속성이 여전히 'oa' 변수에 의해 참조되므로 메모리를 해제할 수 없습니다.

oa = null;
// 'oa' 변수에 다른 값을 할당했고,
// 이제 맨 처음 'o' 변수가 참조했던 객체를 참조하는 다른 변수는 없으므로 가비지 콜렉션이 수행됩니다.

 

 

 

한계: 순환 참조

이 알고리즘은 두 객체가 서로를 참조하면 문제가 발생합니다. 두 객체 모두 더 이상 사용하지 않더라도 가비지 컬렉션을 수행할 수 없게 됩니다.

function f() {
  var x = {}; 
  var y = {};

  x.a = y; // x의 속성 a가 y에 담긴 객체를 참조합니다. 
  y.a = x; // y의 속성 a가 x에 담긴 객체를 참조합니다. 
  return 'azerty';
}

f(); 

위 코드에서 f 함수가 종료되고 나면, x, y에 저장한 객체는 사용되지 않으므로 가비지 컬렉션이 수행되어야 합니다. 그러나 Reference-counting(참조-세기) 알고리즘에서는 두 객체 모두 참조를 가지고 있기 때문에 가비지 콜렉션이 수행되지 않습니다.

😱

 

 

Mark-and-sweep 알고리즘

이 알고리즘에서는 "더 이상 필요 없는 객체"를 "닿을 수 없는 객체"로 정의합니다. 이름에서 알 수 있듯이 무엇인 가에 표시(Mark)를 하고, 정리하는(Sweep) 알고리즘입니다.

이 알고리즘은 roots라는 객체의 집합을 가지고 있습니다.(자바스크립트에서는 전역 변수들을 의미합니다.)

주기적으로 가비지 콜렉터는 roots로부터 시작하여 roots가 참조하는 객체들, roots가 참조하는 객체가 참조하는 객체들을 접근할 수 있는 객체라고 표시합니다. 그 후, 접근할 수 없는 객체에 대해 가비지 컬렉션을 수행합니다.

이 알고리즘은 "참조되지 않는 객체"는 모두 "접근할 수 없는 객체"이지만 은 성립하지 않기 때문에 참조-세기 알고리즘보다 효율적이라고 할 수 있습니다.

2012년 기준, 모든 최신 브라우저들이 가비지 컬렉션에서 Mark-and-sweep 알고리즘을 사용한다고 합니다. 그 후로도 연구되고 있는 자바스크립트 가비지 콜렉션 알고리즘들은 대부분 이 알고리즘을 연구하여 개선한 것이라고 합니다. 그리고 개선된 알고리즘들도 여전히 "더 이상 필요 없는 객체"를 "닿을 수 없는 객체"로 정의하고 있습니다.

 

 

 

순환 참조 문제 해결

참조-세기 알고리즘에서 문제가 됐던 위 코드를 다시 보겠습니다.

function f() {
  var x = {}; 
  var y = {};

  x.a = y;
  y.a = x;
  return 'azerty';
}

f(); 

위 코드에서 함수 f가 리턴되고 나면, 전역 변수들에서 x, y에 담긴 객체들에 접근할 수 있는 방법이 없습니다. 따라서 두 객체에 대해 가비지 컬렉션이 수행될 수 있습니다.

😄

 

 

동작 방식

이 알고리즘은 아래 두 단계로 작동합니다.

  1. Mark

    객체가 생성될 때마다 mark bit가 0 (false)로 설정됩니다.
    Mark 단계에서 모든 접근 가능한 객체의 mark bit가 1 (true)로 설정됩니다.
  1. Sweep

    Mark 단계 후에 mark bit가 여전히 0 (false)로 설정된 객체들은 도달할 수 없는 객체이므로 가비지 콜렉터가 수집해 메모리에서 해제됩니다.

 

 

Mark-and-sweep 알고리즘

 

 

 


 

자바스크립트에서 메모리 관리를 가비지 콜렉터가 한다. 정도의 개념만 알고 있었는데 이번 기회를 통해 가비지 콜렉터의 동작 방식 및 메모리 누수가 발생할 수 있는 상황 등에 대해 개략적으로 공부해 보았습니다.

 

면접 스터디를 진행하며 간과하고 지나갔던 개념들에 대해 다시 한번 짚어보고 넘어갈 수 있게 되어 좋은 경험을 할 수 있었습니다.

 

 

 

끝까지 읽어주셔서 감사합니다.

 

 

참고

 

Comments