가비지 컬렉션

Jun·2022년 12월 10일
0

JavaScript

목록 보기
13/13

https://timurdev.com/javascript-garbage-collection-simple-explanation/

가비지 컬렉션이란?

자바스크립트는 원시값, 객체, 함수 등 우리가 만드는 모든 값들을 메모리에 저장한다. 이렇게 저장한 것들을 사용한 뒤 필요가 없어졌을 때 자동으로 메모리에서 제거하게 된다. 이를 가바지 컬렉션이라고 한다.

왜 우리는 가비지 컬렉션에 대해 알아야 하나요?

위에서도 말했듯이 필요가 없어진 값들은 자바스크립트가 알아서 관리해준다. 이 때문에 자바스크립트 개발자는 메모리 관리에 생각할 필요가 없다고 생각할 수 있는데 이는 잘못된 생각이다.

오히려 이를 알아야 개발을 진행하며 생기는 메모리 누수나 예기치 않은 에러 등을 방지하는데 도움을 줄 수 있다.

메모리 생존주기(Life Cycle)

우선 가비지 컬렉션의 대상인 메모리를 먼저 알아보자.

메모리는 다음과 같은 생존 주기를 가진다.

  1. 필요할 때 할당한다.
  2. 할당된 메모리를 사용한다.
  3. 더 이상 필요가 없으면 해제한다.

1. 필요할 때 할당한다.

https://fe-developers.kakaoent.com/2022/220519-garbage-collection/

개발자가 메모리를 할당해야하는 Low-level 언어와 달리 자바스크립트는 값을 선언할 때 자동으로 메모리를 할당한다. 원시 타입의 값들은 스택 영역에 저장되고, 참조 타입의 값은 힙에 저장되며, 그 주소값은 스택영역에 저장된다.

const a = 1; // 원시 타입은 메모리 스택 저장

const b = [1,2,3]; // 참조 타입은 힙 영역에 저장

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

2. 할당된 메모리를 사용한다.

값을 사용한다는 것은 할당된 메모리를 읽고 쓰는 것을 의미한다.

console.log(a); // output: 1
console.log(b[0]); // output: 1

3. 더 이상 필요가 없으면 해제한다.

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

low-level 언어에서는 개발자가 판단하고 결정하여 메모리를 해제한다. 하지만 자바스크립트는 개발자가 수동으로 할 수가 없기에 가비지 컬렉션(이하 GC)이라는 자동 메모리 관리 방법을 사용한다. 그렇다면 GC는 어떤 기준을 가지고 메모리를 필요없다고 판단할까?

가비지 컬렉션의 작동 방식

참조

GC의 핵심 개념은 참조이다. A라는 메모리를 통해 명시적이든 암시적이든 B라는 메모리에 접근할 수 있다면 B는 A에 참조된다 라고한다.

Reference-counting

Reference-counting 알고리즘을 사용하는 GC는 필요없는 객체를 어떤 다른 객체도 참조하지 않는 객체 라고 정의한다. 이를 가비지 라 부르며, 메모리에서 삭제하게 된다.

let x = {
  a: {
    b: 2
  }
};

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

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

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

y = "apple";  // 이제 맨 처음 'y' 변수가 참조했던 오브젝트를 참조하는 오브젝트는 없다.
                // 이제 오브젝트에 GC가 수행될까?

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

맨 처음 변수 x 에 객체를 할당하고 변수 yx 를 참조하도록 한다. 그 후 x 에 새로운 값인 1을 재할당하면 객체는 메모리에서 제거될까? y 가 아직 객체를 참조하고 있기때문에 GC는 객체를 제거하지 못한다.

이제 새로운 변수 zy 가 참조하는 객체의 a 를 할당해보자. 그리고 y 또한 새로운 값인 'apple'을 재할당하면 객체는 메모리에서 제거될까? z 가 내부 객체 a 를 참조하고 있기에 GC는 객체를 제거하지 못한다.

마지막으로znull 을 재할당하게 되면 객체에 대한 참조가 존재하지 않게된다. 어떤 다른 객체도 참조하지 않는 객체 가 되므로 GC는 이를 필요없는 객체라 판단하여 메모리에서 제거하게 된다.

한계

만약 객체가 순환 참조(객체와 객체가 서로 참조하는 구조)형태라면 Reference-counting 방식으론 GC가 불가능해진다.

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

  return "azerty";
}

Circular();

Circula 함수의 호출이 완료된 시점에서 객체 xy 는 스코프를 벗어나게 될 것이고, 두 객체는 불필요해지므로 메모리에서 제거되어야한다. 하지만 함수 내부에서 두 객체가 서로 참조하고 있으므로 Reference-counting 알고리즘은 이를 GC의 대상으로 인식하지 못한다. 흔한 메모리 누수의 예제이다.

이를 해결하기 위해 Mark-and-sweep 알고리즘을 사용한다.

Mark-and-sweep

여기선 GC가 필요없는 객체를 닿을 수 없는 객체로 정의한다. 또한 roots라는 객체의 집합을 가지고 있다.(자바스크립트에선 전역 변수). roots에서 시작해서 roots가 참조하는 객체, 그리고 그 객체를 참조하는 객체들… 을 닿을 수 있는 객체라고 표시한다. GC는 닿을 수 없는 객체에 수행되게된다. 순서를 정리하자면 다음과 같다.

  1. GC는 root 정보를 수집하고 이를 mark(기억) 한다.

  1. root가 참조하고 있는 모든 객체를 방문하고 이를 mark 한다.

  1. mark된 모든 객체에 방문하고 그 객체들이 참조하는 객체도 mark한다.

  1. root에서 도달 가능한 모든 객체를 방문할 때 까지 반복한다.

  1. mark되지 않은 객체(닿을 수 없는 객체)를 메모리에서 제거한다.

최적화 기법

  1. generational collection (세대별 수집)
    객체를 새로운 객체오래된 객체 로 나눈다. 상당수의 객체는 생성 이후 역할을 수행하고 사라지기 때문에 새로운 객체의 경우 GC가 더욱 공격적으로 메모리에서 제거한다. 일정 시간 이상 존재한 객체는 오래된 객체로 분류하고 덜 감시한다.
  2. incremental collection (점진적 수집)
    방문해야 될 객체가 많을 시 모든 객체를 mark 하는데 상당한 시간과 리소스가 필요하다. 이 경우 GC를 여러 부분으로 분리한 뒤 각 부분을 별도로 수행한다.
  3. idle-time collection (유휴 시간 수집)
    실행에 주는 영향을 최소화하기 위해 CPU가 유휴 상태일때만 GC를 수행한다.
profile
FrontEnd Engineer를 목표로 공부합니다.

0개의 댓글