본문 바로가기

JavaScript

자바스크립트의 가비지컬렉션(Garbage Collection)

메모리 관리는 왜 필요한가?

대부분의 언어에서 메모리 라이프 사이클은 메모리 할당 → 메모리 사용 → 메모리 해제의 단계를 거친다. C같은 low-level 언어의 경우 이 라이프 사이클을 개발자가 malloc()이나 free()를 사용하여 직접 관리를 해주어야 하지만 자바스크립트와 같은 high-level 언어는 대부분 Garbage Collection이라는 자동 메모리 관리를 사용하기 때문에 개발자가 별도의 신경을 쓰지 않는다.

 

가비지컬렉션(Garbage Collection)이란?

가비지컬렉션은 더이상 사용하지 않는 메모리를 발견하고 이를 해제해주는 역할을 한다. 가비지컬렉션이 자동으로 메모리 관리를 해준다고해서 개발자가 완전히 신경을 쓰지 않는다면 메모리 누수(memory-leak)가 발생할 수도 있다. 메모리 누수란, 더이상 어플리케이션에서 사용하지 않는도 불구하고 메모리 해제가 되지 않는 상태를 말한다.

 

가비지컬렉션의 주요 알고리즘

가비지컬렉션의 주요 알고리즘으로는 두가지 방식이 있다.

 

1. Reference-counting

이 알고리즘은 어떠한 값에 대해서 어디에서도 참조(reference)하지 않고 있다면 GC는 이 값을 필요하지 않은 값으로 간주하고 이 값을 제거한다.

 

예시를 살펴보자.

// people 변수는 해당 object를 참조한다 .
let people = {
  name: "Jane"
};
//null값을 재할당한다.
people = null;

{name: "Jane"}이라는 값은 더이상 참조되지 않기 때문에 GC에 의해 메모리가 해제된다.

 

// people 변수는 해당 object를 참조한다 .
let people = {
  name: "Jane"
};
//girl에 people의 참조중인 값의 주소를 할당한다.
girl = people;
//people에 null을 할당한다.
people = null;

people은 더이상 {name: "Jane"}을 참조하고 있지 않지만, girl은 참조하고 있기 때문에 해당 object는 여전히 메모리에 남아있게 된다.

 

// people 변수는 해당 object를 참조한다 .
let people = {
  name: "Jane"
};
//girl에 people의 참조중인 값의 주소를 할당한다.
girl = people;
//people에 null을 할당한다.
people = null;
//girl에 null을 할당한다.
girl = null;

만약 위의 코드처럼 people, girlnull을 할당하고 나면 {name: "Jane"}를 참조하고 있는 곳이 없기 때문에 해당 값은 GC에 의해 처리된다.

 

하지만 reference-counting 방식은 순환참조(Circular reference)가 이루어지는 경우 메무리 누수의 요인이 된다는 문제점이 있다. 아래 예시 코드를 보자.

function couple() {
    const jane = {};
    const sam = {};

    // jane.bf는 sam을 참조한다
    jane.bf = sam;

    // sam.gf는 jane을 참조한다
    sam.gf = jane;

    return 'circular';
}

couple();

couple()함수가 호출되고 끝나서 더이상 필요한 값이 아닌데도 불구하고 서로에 대한 참조가 걸려있기 때문에 GC는 이 값들에 대한 메모리를 해제하지 않아 계속해서 메모리에 남아 있는다.(live happily together forever..) 이러한 순환참조는 메모리 누수의 주된 요인이라고 할 수 있다.

 

IE6와 IE7은 DOM object에 reference-counting GC를 사용하는 것으로 알려져있는데, 아래의 경우는 메모리 누수를 일으키는 주된 요인이 된다. 아래에는 현실세계에서 사용될 법한 코드 예시이다.

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

myDivElementcircularReference에 자기 자신의 값을 참조하고 있다. 따라서 DOM 트리에서 해당 element가 제거되었다고 하더라도 여전히 reference는 남아있기 때문에 GC는 이 값의 메모리를 해제하지 않는다. 특히나 순환참조되고 있는 값이 사이즈가 큰 데이터를 가지고 있다면 이 메모리는 해제될 수 없기 때문에 브라우저가 느려지거나 하는 이슈가 생길 수 있다.

 

2. Mark-and-sweep 

위의 reference-counting의 문제점으로는 불필요한 값임에도 불구하고 어디에선가 참조가 걸려있다면 이에 대한 메모리를 해제하지 않는다는 문제점이 있었다. 반면에 Mark-and-sweep 알고리즘은 이 값이 참조되고 있는 값인지에 중점을 두지않고 도달가능성(reachablility)에 중점을 둔다.

 

도달 가능성이란 자바스크립트의 root라는 글로벌 object에서부터 시작하여 참조되는가에 대한 여부이다. 다시말해 root에서 부터 해당 값까지 도달이 가능한가에 대한 여부이다.

 

따라서 어떤 값에 대한 참조가 없는 경우는 당연히 도달이 불가능하기 때문에 메모리가 해제되어야 하는 값으로 여겨지고, 참조 되고 있다고 하더라도 root로부터 도달할수 없다고 여겨진다면 처리된다.

도달되지 않은 값의 메모리는 GC에 의해 해제된다.

 

 

 

2012년부터 모던 브라우저들은 mark-and-sweep 방식의 GC를 사용하고 있다. reference-counting 방식에서 문제가 되었던 아래의 예시를 다시 살펴보자.

function Couple() {
    const jane = {};
    const sam = {};

    // jane.bf는 sam을 참조한다
    jane.bf = sam;

    // sam.gf는 jane을 참조한다
    sam.gf = jane;

    return 'circular';
}

Couple();

예시에서 Couple() 이라는 함수 가 호출된 후 'circular'값이 return 되고 끝난 후에는 더이상 root에서 jane과 same에 도달할 수 없기 때문에 해당 값들은 GC에 의해서 메모리가 해제된다.

 

결론

  • 어떠한 값이 참조된다고해서 root에서부터 도달가능한 것은 아니다.
  • reference-counting의 순환 참조 문제점을 mark-and-sweep방식이 보완할 수 있다.
  • GC는 자동으로 실행되며 강제로 멈추거나 실행시킬 수 없다.

 

참고링크

https://javascript.plainenglish.io/memory-leaks-and-garbage-collection-️-in-javascript-you-need-to-know-this-3fa8173e8b3c

https://javascript.info/garbage-collection

https://www.geeksforgeeks.org/garbage-collection-in-javascript/

https://www.youtube.com/watch?v=AeUCN2lPqL8&t=72s

https://stackoverflow.com/questions/7347203/circular-references-in-javascript-garbage-collector

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management