가비지컬렉터의 역할과 동작방식

Shin Yeongjae·2023년 7월 5일
0

TIL

목록 보기
19/21

이전 블로그에서 글 옮김

TL;DR

가비지컬렉터의 역할은 자바스크립트 엔진이 메모리 할당을 모니터링하고 할당된 메모리의 블록이 더 이상 필요하지 않은 시점을 확인하여 회수하는 것이다.

가비지컬렉터는 Reference-counting알고리즘과 Mark-and-sweep 알고리즘에 따라 동작한다.
1. 참조-세기(Reference-counting)

  • 참조-세기는 더 이상 필요없는 오브젝트를 어떤 다른 오브젝트도 참조하지 않는 오브젝트라고 정의한다. 이 오브젝트를 가비지라고 부르며, 이를 참조하는 다른 오브젝트가 하나도 없는 경우 수집이 가능하다.
  • 이 알고리즘은 순환 참조의 문제점을 가지고 있다.
  1. 표시하고-쓸기(Mark-and-sweep)
    • Mark: 객체가 생성될 때마다 mark bit0(false)으로 설정된다. mark 단계에서 접근 가능한 객체의 mark bit1(true)로 설정된다.
    • Sweep: mark 단계 후 mark bit가 여전히 0으로 설정된 객체들은 도달할 수 없는 객체이므로 가비지컬렉터가 수집해 메모리에서 해제된다.
    • 이 알고리즘은 참조-세기알고리즘의 문제점을 보완할 수 있어 2012년 이후 대부분의 브라우저에서 채택하고 있다.

가비지컬렉션은 자동으로 실행되며 강제로 멈추거나 실행시킬 수 없다.

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

C 언어 같은 저수준(로우레벨) 언어에서는 메모리 관리를 위해 malloc()free()를 사용한다고 합니다. 반면에, 자바스크립트는 눈에 보이지 않는 곳에서 메모리 관리를 수행합니다.
객체가 생성되었을 때 자동으로 메모리를 할당하고 쓸모 없어졌을 때 자동으로 해제합니다.(가비지 컬렉션)

원시값, 객체, 함수 등 우리가 만드는 모든 것은 메모리를 차지합니다. 그럼 쓸모 없어지게 된 것들은 자동으로 해제 된다는데 어떤 기준에 의해 해제되는 것일까요?

가비지컬렉션

자바스크립트의 가비지컬렉션 기준을 알아보기 전에 메모리의 생존주기와 자바스크립트에서의 메모리 할당 대해 알아보겠습니다.

메모리의 생존주기

메모리의 생존주기는 저수준 언어, 고수준 언어와 관계없이 비슷합니다.

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

2번은 모든 언어에서 명시적으로 사용되지만 1번과 3번은 저수준 언어에서는 명시적이며, 자바스크립트와 같은 고수준(하이레벨) 언어에서는 암묵적으로 작동합니다.

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

값 초기화

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

var num = 123; // 정수를 담기 위한 메모리 할당
var str = '123'; // 문자열을 담기 위한 메모리 할당

var obj = {
  a: 123,
  b: null,
}; // 객체와 객체에 포함된 값들을 담기 위한 메모리 할당

var arr = [123, null, '123']; // 배열과 배열에 담긴 값들을 위한 메모리 할당

function foo(a) {
  return a + 1;
} // 함수를 위한 할당(함수는 호출 가능한 객체입니다)

// 함수표현식 또한 객체를 담기위한 메모리를 할당합니다
someElement.addEventListener('click', function() {
  someElement.style.backgroundColor = 'red';
}, false);

함수 호출을 통한 할당

함수 호출의 결과 메모리 할당이 일어나기도 합니다.

var date = new Date(); // Date 객체를 위해 메모리 할당
var el = document.createElement('div'); // DOM Element를 위해 메모리 할당

메소드가 새로운 값이나 객체를 할당하기도 합니다.

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

var arr = ['123', '456'];
var arr2 = ['789', '101112'];
var arr3 = arr.concat(arr2);
// arr과 arr2를 합친 4개의 원소를 가진 새로운 배열

자바스크립트의 가비지컬렉션 기준

쉽게 말하면 어떤 값들이 더 이상 도달이 불가능한 경우 가비지컬렉션의 대상이 됩니다.

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

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

아래의 값들은 태생부터 도달 가능하기 때문에 이유 없이 삭제되지 않습니다.

  • 현재 함수의 지역 변수와 매개변수
  • 중첩 함수의 체인에 있는 함수에서 사용되는 변수와 매개변수
  • 전역 변수

이런 값은 루트(root)라고 부릅니다.
루트가 참조하는 값이나 체이닝으로 루트에서 참조할 수 있는 값은 도달 가능한 값이 됩니다.

전역 변수에 객체가 저장되어있다고 가정해보면, 이 객체의 프로퍼티가 또 다른 객체를 참조하고 있다면, 프로퍼티가 참조하는 객체는 도달 가능한 값이 됩니다.
따라서 이 객체가 참조하는 다른 모든 것들도 도달 가능하다고 여겨집니다.

내부 알고리즘

1. 참조-세기(Reference-counting) 알고리즘

간단한 예시

// user엔 객체 참조 값이 저장됩니다.
let user = {
  name: 'John'
};

이 그림에서 화살표는 객체 참조를 나타냅니다. 전역 변수 user{ name: 'John' }이라는 객체를 참조합니다.
user의 값을 다른 값으로 덮어쓰면 참조가 사라집니다.

user = null;

John은 도달할 수 없는 상태가 되었기 때문에 가비지 컬렉터(이하 GC)가 John에 저장된 데이터를 삭제하고, John을 메모리상에서 삭제합니다.

참조 두 개

참조를 user에서 admin으로 복사했다고 가정해봅시다.

// user엔 객체 참조 값이 저장됩니다.
let user = {
  name: 'John'
};

let admin = user;

그리고 위에서 한 것 처럼 user의 값을 다른 값으로 덮어써 봅니다.

user = null;

전역 변수 admin을 통하면 여전히 객체 John에 접근할 수 있기 때문에 John은 메모리상에서 삭제되지 않습니다. 이 상태에서 admin을 다른 값으로 덮어쓰면 John은 메모리상에서 삭제될 수 있습니다.

연결된 객체

조금 복잡한 예시가 있습니다.

function marry(man, woman) {
  woman.husband = man;
  man.wife = woman;
  
  return {
    father: man,
    mother: woman,
  }
};

let family = marry({
  name: 'John',
}, {
  name: 'Ann',
});

메모리 구조는 아래와 같이 나타낼 수 있습니다.

위 예시의 함수는 호출이되고 끝나서 더 이상 필요한 값이 아닌데도 파라미터로 받은 두 객체를 서로 참조하게 되면서(순환 참조) GC는 이 값들에 대한 메모리를 삭제하지 않아서 메모리에 계속 남아있게 됩니다.
순환참조는 메모리 누수를 일으키는 주된 요인이라고 할 수 있습니다.

참조 두 개를 지워보도록 하겠습니다.

delete family.father;
delete family.mother.husband;

삭제한 두 개의 참조 중 하나만 지웠다면, 모든 객체가 여전히 도달 가능한 상태였지만 두 개를 지우면 John으로 들어오는 참조는 모두 사라져
John은 도달 가능한 상태에서 벗어나 GC에 의해 메모리상에서 삭제됩니다.

도달할 수 없는 섬

객체들이 연결되어 섬 같은 구조를 만드는데, 이 섬에 도달할 수 없으면 섬을 구성하는 객체 전부 메모리상에서 삭제됩니다.
family가 아무것도 참조하지 않도록 만들어 봅시다.

family = null;

John과 Ann은 여전히 서로를 참조하고 있고, 두 객체 모두 외부에서 들어오는 참조를 가지고 있습니다.
하지만 fmaily객체와 루트의 연결이 사라지면 루트 객체를 참조하는 것이 아무것도 없게 됩니다. 섬 전체가 도달할 수 없는 상태가 되어
섬을 구성하는 객체 모두가 메모리상에서 삭제됩니다.

도달할 수 없는 섬 예제는 도달 가능성이라는 개념이 얼마나 중요한지 보여줍니다.

2. 표시하고-쓸기(Mark-and-sweep) 알고리즘

Mark-and-sweep알고리즘은 다음 단계를 거쳐 수행됩니다.
1. GC는 루트 정보를 수집하고 이를 mark(표시)합니다.
2. 루트가 참조하고 있는 모든 객체를 방문하고 이것들을 mark합니다.
3. mark된 모든 객체에 방문하고 그 객체들이 참조하는 객체도 mark합니다. 한번 방문한 객체는 전부 mark하기 때문에 같은 객체를 다시 방문하지는 않습니다.
4. 루트에서 도달 가능한 모든 객체를 방문할 때까지 위의 과정을 반복합니다.
5. mark되지 않은 모든 객체를 메모리상에서 삭제합니다.

간단한 예시

function couple() {
  const John = {};
  const Ann = {};
  
  // John.girlFriend는 Ann을 참조한다. 
  John.girlFriend = Ann;

  // Ann.boyFriend는 John을 참조한다.
  Ann.boyFriend = John;
  
  return '순환참조';
};

couple();

위 예시에서 couple()이라는 함수가 호출된 후 '순환참조'가 return 되고 함수가 끝난 후에는 더 이상 root에서 John과 Ann에 도달할 수 없기 때문에
해당 값들은 GC에 의해서 메모리상에서 삭제됩니다.

2012년부터 모던 브라우저들은 대부분 GC에 Mark-and-sweep알고리즘을 사용합니다.


Reference

profile
문과생의 개발자 도전기

0개의 댓글