[자바스크립트] 가비지 컬렉팅

Woonil·2025년 8월 7일
0

자바스크립트

목록 보기
6/8
post-thumbnail

Garbage Collecting을 학습하기 전에 먼저 Garbage Collector(가비지 컬렉터)의 대상인 메모리에 대해 알아야 한다. 메모리는 Allocate memory(메모리 할당) → Use memory(메모리 사용) → Release memory(메모리 해제) 의 생명주기를 가진다. Garbage란 더 이상 사용되지 않지만 release되지 않는 메모리를 의미하며, 이는 Garbage Collecting의 대상이 된다.

자바스크립트는 ‘도달 가능성(reachability)’이라는 개념을 사용해 메모리 관리를 수행한다. ‘도달 가능한’값은 어떻게든 접근하거나 사용할 수 있는 값을 의미하며, 도달 가능한 값은 메모리에서 삭제되지 않는다.

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

  • 예시
    let person = {
    	name: 'Wonil'
    };
    person = null;
    person 을 다른 값으로 덮어씌었기 때문에 참조가 사라지며, 이제 해당하는 객체는 도달할 수 없는 상태가 되었다. 이렇게 되면 가비지 컬렉터는 객체에 저장된 데이터를 삭제하고

🤔개념

루트

가비지 컬렉터가 관리하는 특수한 객체로, 태생부터 도달 가능하여 외부에서 내부의 객체에 접근할 수 있도록 해주는 시작 지점이 되어준다. 루트는 명백한 이유 없이는 삭제되지 않는다.

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

루트가 참조하는 값이나 체이닝으로 루트에서 참조할 수 있는 값은 도달 가능한 값이 된다.

연결된 객체와 도달할 수 없는 섬

function marry(man, woman) {
  woman.husband = man;
  man.wife = woman;

  return {
    father: man,
    mother: woman
  }
}

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

가비지 컬렉션 과정

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

두 개의 참조를 삭제하니, John(이름의) 객체로는 도달이 불가능해졌다.

가비지 컬렉션 후의 메모리 구조는 아래와 같다.

예시에서는 객체들이 연결되어 섬 같은 구조를 형성하였다. 이때, 근원 객체인 family가 아무것도 참조하지 않는 상황이 발생해도 해당 섬에 도달할 방법이 없으므로, 이때는 객체 전부가 메모리에서 삭제된다.

family = null;

[사진 출처: https://ko.javascript.info/garbage-collection]

내부 알고리즘

Referencing Counting

객체는 본인을 참조하고 있는 개수를 내부적으로 저장한다. 하지만 이 알고리즘의 경우, 순환 참조가 발생할 수 있어 위에서 언급한 ‘도달할 수 없는 섬’이 발생할 수 있다.

Mark and Sweep

Referencing Counting의 순환 참조 문제를 해결할 수 있는 알고리즘으로, mark란 ‘표시하다’란 뜻이고, sweep은 ‘쓸다’라는 의미를 가지고 있다.

  • 순서
    1. 가비지 컬렉터는 루트 정보를 수집하고 이를 표시(mark)한다.
    2. 루트가 참조하고 있는 모든 객체를 방문하고 이것들을 표시(mark)한다.
    3. 표시(mark)된 모든 객체에 방문하고, 그 객체들이 참조하는 객체도 표시(mark)한다.
    4. 한번 방문한 객체는 전부 표시(mark)하기 때문에 같은 객체는 다시 방문하지 않는다.
    5. 루트에서 도달 가능한 모든 객체를 방문할 때까지 위 과정을 반복한다.
    6. 표시(mark)되지 않은 모든 객체를 메모리에서 삭제한다. 즉, 쓸어내는(sweep) 것이다.

메모리 누수를 일으키는 요인

전역 변수

프로그래밍을 할 때, 전역 변수의 사용을 최소화하라고 많이들 말한다. 전역 변수의 경우, GC Root와 가까운 곳에 직접 생성이 되기 때문에 별도로 메모리에서의 해제 과정을 수행하지 않는다면 계속해서 GC Root에서 접근 가능한 곳에 위치하게 된다. 즉, 메모리에 계속해서 남아있게 되는 것이다.

var, const, let 키워드 없이 변수를 선언하면 기본적으로 전역변수(브라우저의 경우 window, node의 경우 global)의 프로퍼티로 할당된다.

function foo1() {
	bar = "some text";
}
foo1();
console.log(window.bar);

// is the equivalent of
function foo2() {
	window.bar = "some text";
}
foo2();
console.log(window.bar);

function foo3() {
	this.var1 = "potential accidental global";
}
foo3();
console.log(window.var1);

bar 함수의 실행이 끝났음에도 불구하고, 브라우저의 전역 객체인 window 에 할당된, 즉 전역적으로 선언된 변수이기 때문에 메모리에서 해제되지 않는다. 따라서, 변수 선언은 특별한 경우를 제외하고는 var, const, let 키워드와 함께 행해져야 한다.

잊혀진 타이머 또는 콜백

var serverData = loadData();
setInterval(function() {
    var renderer = document.getElementById('renderer');
    if(renderer) {
        renderer.innerHTML = JSON.stringify(serverData);
    }
}, 5000);

더 이상 serverData 가 불리지 않더라도 등록된 시간 간격마다 참조되기 때문에, 메모리에서 해제되지 않는다. 따라서 더 이상 사용되지 않는 타이머는 clearInterval (setTimeout의 경우 clearTimeout)로 해제해주어야 한다.

// 메모리 해제를 잘한 예시
var element = document.getElementById('launch-button');
var counter = 0;
function onClick(event) {
   counter++;
   element.innerHtml = 'text ' + counter;
}
element.addEventListener('click', onClick);
// Do stuff
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);

클로저

var theThing = null;

var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing) // 'originalThing'에 대한 참조
      console.log("hi");
  };
  
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log("message");
    }
  };
};
setInterval(replaceThing, 1000);

DOM에서 벗어난 요소 참조

var elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image')
};

function doStuff() {
    elements.image.src = 'http://example.com/image_name.png';
}

function removeImage() {
    // image는 body 요소의 바로 아래 자식임
    document.body.removeChild(document.getElementById('image'));
		// 이 순간까지 button 전역 요소 객체에 대한 참조가 여전히 존재함
		// 즉, button 요소는 아직도 메모리 상에 남아있고, 가비지 컬렉터가 수집할 수 없음
}

참고자료

가비지 컬렉션
[JavaScript] 가비지 컬렉터란? 1편 어떻게 동작하나
[JavaScript] 가비지 컬렉터란? 2편 메모리 누수를 방지하는 팁!
자바스크립트 v8 엔진의 가비지 컬렉션 동작 방식 | 카카오엔터테인먼트 테크블로그
[개발면접3분] Mark and Sweep 가비지 컬렉션 기법 (feat. Reference count)
How JavaScript works: memory management + how to handle 4 common memory leaks

profile
프론트 개발과 클라우드 환경에 관심이 많습니다:)

0개의 댓글