[CS]가비지 컬렉션과 동작 방식

신세원·2021년 10월 3일
0

CS

목록 보기
3/8
post-thumbnail

자바스크립트를 공부하는 사람들이라면 누구나 한번 그 이상은 들어봤을만한 단어이고,
대기업 뿐만 아니라 각종 회사 면접 질문에서 단골손님으로 등장하는 가비지컬렉션에 대해 정리 해보기로 한다.

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

영어 단어에서 Garbage는 곧 쓰레기를 의미하는데 자바스크립트에서 쓰레기란 무엇을 뜻하는 것일까? 그 뜻은 아래와 같다.

가비지란, 사용되어 지지 않는 메모리이지만, 방출되지 않은 release 되지 않은 메모리를 뜻한다.

그럼 위에서 말하는 사용은 무엇인고, 방출은 무엇을 뜻하는 것일까?
그것은 바로 아래와 같은 메모리의 라이프 싸이클을 의미한다.

메모리는 위처럼 3단계의 라이프싸이클을 거치게 되는데, 쉽게 이야기해서,

  • 메모리를 할당하고,메모리를 사용하고,메모리를 방출한다.

그러면 여기서 조금 더 구체적으로 어떻게 Allicate(할당)되는지 알아봄으로써, 가비지컬렉션이 어떻게 동작하는지 알 수 있다.

2. 메모리(Memory)

메모리는 간단히 이야기해서, 0,1로 표현되어지는 어떠한 값이 어딘가에 저장이 되어져있고, 저장되어 있는 그 주소를 의미한다.

2-1. Allicate(메모리 할당)

좀 더 이해하기 자바스크립트에서 첫 번째 단계인 메모리 할당이 어떻게 작동하는지 알아보자.
자바스크립트는 개발자들을 메모리 할당의 책임에서 해방시켜주었고, 자바스크립트는 변수 할당 시점에 메모리 할당을 스스로 수행한다.

let n = 365; // 숫자에 대한 메모리 할당

let s = 'abc'; // 문자에 대한 메모리 할당

let o = {
  a: 10,
  b: null
}; // 객체 및 그 값에 대한 메모리 할당

let a = [1, null, 'str'];  // (객체와 같음) 배열과 그 값에 대한 메모리 할당

let func=(a)=> a+3 // 함수에 대한 할당(함수는 호출할 수 있는 객체임)

// 함수 표현식 또한 객체를 할당
someElement.addEventListener('click', ()=> {
  someElement.style.backgroundColor = 'blue';
}, false);

아래와 같은 몇몇 함수 호출은 객체 할당의 결과를 가져오기도 한다.


const day = new Date(); // Date객체 할당
const element = document.createElement('div'); // DOM 요소 할당

메소드는 새로운 값이나 객체를 할당할 수 있다.

let s1 = 'sseni';
let s2 = s1.substr(0, 3); // s2는 새로운 문자열이 됨
// 문자열은 불변(immutable)이므로 자바스크립트는 메모리를 할당하지 않고 ,[0, 3]의 범위만 저장할 수도 있음
let a1 = ['Shin', 'sseni'];
let a2 = ['Lee', 'KongIl'];
let a3 = a1.concat(a2);  // 4개의 요소를 가진 새로운 배열은 a1과 a2 요소의 연결이 됨

2-2. Use(메모리 사용)

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

2-3. Release(메모리 방출)

위에서 언급한 내용처럼 가비지란 사용되어 지지 않는 메모리이지만, 방출되지 않은 release 되지 않은 메모리를 뜻한다.
C언어에서는 가비지를 자동으로 해주는 함수가 있는데, 자바스크립트와 같은 고수준의 언어에는 가비지컬렉터(garbage collector)라는 소프트웨어가 내장되어 있어, 가비지컬렉터(garbage collector)메모리 할당을 추적하고 언제 할당된 메모리가 더 이상 사용되지 않는지 파악해서 자동으로 회수한다.

3. 가비지 컬렉션 알고리즘과 한계점

먼저 가비지를 회수하여 사용할 수 있는 메모리 공간을 늘리는 작업을 가비지 컬렉션이라 하고, 그리고 이러한 일을 수행하는 것을 가비지 컬렉터라고 하는데, 가비지 컬렉터가 자동으로 가비지를 회수하다보니 여기서 문제점이 발생한다.

수거하는 메모리가 더이상 필요하지 않는 메모리인지 아닌지 어떻게 확인하지?

그렇다. 우리는 어떤 메모리가 수거 대상이 되어야하는지 아닌지 판별을 해서 정의를 해야한다.
그렇기에 이번에는 메모리의 판별 방법에 대해서 설명하고자 한다.

3-1. Memory References

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

정리하자면 어떤 메모리가 참조되어 있는지, 또 그것을 가지고 이 메모리가 수거 대상이 맞는지, 아닌지를 가비지컬렉션은 판별하여 reference를 카운팅하여 더이상 참조가 일어나지 않는 변수는 수거해간다.

참조횟수계산 가비지컬렉션
이것은 가장 단순한 형태의 가비지컬렉션 알고리즘이다. 객체는 만약 그것을 가리키는 참조가 하나도 없는 경우 가비지컬렉션 대상(garbage collectible)으로 간주된다.

이러한 설명을 바탕으로 아래 코드를 살펴보자.

let obj1 = {  
 obj2: {         
   a: 1         
 }
};

위 코드에서는 두 객체가 생성이 되었고, 'obj2'는 'obj1'이 자신의 속성으로서 참조되고 있다.
여기서 각 객체는 모두 참조되고 있기 때문에 가비지 컬렉션이 수거해야할 객체는 없다.


let obj3 = obj1; // 'obj3' 변수는 'obj1'이 가리키는 오브젝트에 대한 참조를 갖는 두 번째 변수이다.

obj1 = 1;      // 이제 'obj1'에 있던 객체는 하나의 참조만 남게 되고,  그것은 'obj3' 변수에 들어 있다.

let obj4 = obj3.obj2; // 'obj2' 속성에 대한 참조
               // 이제 이 객체는 하나는 속성으로서, 다른 하나는 'obj4' 변수로서 두개의 참조를 가진다.

obj3 = '374'; // 원래 'obj1'에 있던 객체는 이제 참조를 하는 곳이 없어 가비지컬렉션이 수행 될 수 있다.
           // 하지만 'obj2' 속성은 'obj4' 변수가 참조하므로 가비지컬렉션이 수행 될 수 없다.

obj4 = null; // 원래 'obj1'객체 내에 있던 'obj2'속성은 이제 참조하는 곳이 없으므로, 가비지컬렉션이 수행된다.

Memory References의 한계점


function f() {
  let obj1 = {};
  let obj2 = {};
  obj1.p = obj2; // obj1은 obj2를 참조한다.
  obj2.p = obj1; // obj2는 obj1을 참조함. 이를 통해 순환 참조가 만들어짐.
}
f();

순환참조를 한마디로 정의하자면 cycles이다. 간단하게 이야기하면 obj1과 obj2가 서로 참조 한다는 뜻이다. 이렇게 서로 참조하게 되어 버리면 절대 reference가 0이 될 일은 없다.
그러면 이러한 obj들은 가비지로 인식이 되지 않아 수거대상이 되질 않고, 메모리가 release가 되지 않는 한계점이 발생한다.

이 순환되는 사이클의 한계점을 극복하기 위해 두번째 방법을 채택하게 된다.

3-2. Mark-and-sweep 알고리즘

객체가 필요한지 결정하기 위해서 이 알고리즘은 해당 객체에 닿을 수 있는지(reachable)를 판단하여, "닿을 수 없는 객체"로 정의한다.

더 쉽게 정리하자면 어떤 것이 가비지 수거 대상인지 마크를 먼저 한다.로 정의 할 수 있다.

마크스위프 알고리즘은 다음의 단계를 거치게된다.

  • 가비지 컬렉터는 루트(root) 정보를 수집하고 이를 ‘mark(기억)’ 한다.
  • 루트가 참조하고 있는 모든 객체를 방문하고 이것들을 ‘mark’ 한다.
  • mark 된 모든 객체에 방문하고 그 객체들이 참조하는 객체도 mark 한다. 한번 방문한 객체는 전부 mark 하기 때문에 같은 객체를 다시 방문하는 일은 없다.
  • 루트에서 도달 가능한 모든 객체를 방문할 때까지 위 과정을 반복한다.
  • mark 되지 않은 모든 객체를 메모리에서 삭제한다.

위와 같은 동작 방식은 다음과 같다.

Mark

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

Sweep

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

이 알고리즘은 "참조되지 않는 객체"는 모두 "닿을 수 없는 객체"이지만, 순환 참조에서처럼 역은 성립하지 않기 때문에 참조-세기 알고리즘보다 효율적이다.

2012년 기준, 모든 현대적인 브라우저들은 Mark-and-sweep 알고리즘을 사용한 가비지 컬렉터를 장착하고 있다고 한다. 그 후로도 지난 몇 년 간 자바스크립트 가비지 컬렉션 분야에서 있었던 모든 개선은 가비지 콜렉션 자체나 어떤 객체가 닿을 수 있느냐를 판별하는 것에 있었던 것이 아니라 바로 이 Mark-and-sweep 알고리즘의 구현에 대한 개선이었다.

순환 참조 문제 해결

그럼 위에서 봤던 참조-세기 알고리즘에서 문제가 되었던 부분들을 다시 보도록 하자.


function f() {
  let obj1 = {};
  let obj2 = {};
  obj1.p = obj2;
  obj2.p = obj1; 
}
f();

이제 함수 호출에 값을 반환한 다음 두 객체는 서로에 대한 참조가 있기는 하지만 루트에서는 닿을 수 없는 상태이기에, 더 이상 전역 객체로부터 접근할 방법이 없게 되었다. 따라서, 두 객체에 대해 가비지 컬렉션이 수행될 수 있다. 그렇기에 이 전에서 보았던 reference 카운팅의 한계점이었던 cycles를 해결할 수 있게 되었다.

Mark-and-sweep의 한계점

Mark-and-sweep에도 한계점이 존재하는데, 바로 가비지 컬렉터가 가비지를 언제 수거해가는지 우리가 알 수 없다는 것이다.

자세한 한계를 알기 위해서는 시나리오를 가정해봐야 한다.

  • 굉장히 많은 수의 변수를 메모리에 할당한다.
  • 메모리 할당이 수행되어 질때 이 대부분의 요소들은 unreachable(닿을 수 없음)의 상태로 표시되어, 더 이상의 메모리 할당이 일어나지 않는다.

Mark-and-sweep 한계를 알아보기전, Mark-and-sweep의 특징은 메모리에 할당이 되어야 최상위 GC Root들이 생성 되어진다. 반대로 메모리에 할당이 되지 않으면 Root를 만들지 않는다.
그렇게 때문에 어떤게 가비지 인지 확인할 수가 없다.

그럼 다시 시나리오로 돌아가서 처음에 메모리에 할당이 잔뜩 일어나서, 더이상 할당이 일어나지 않으면 처음 할당되었던 메모리 중에서 가비지로 판단되어지는 메모리들이 있음에도 불구하고, 어떤게 가비지 수거대상인지 알 수가 없어, 메모리들을 수거해가지 않게 된다.

그렇기에 이러한 상황에서 발생하는것이 바로 Memory Leak(메모리 누수)이다.

이렇게 발생되는 Memory Leak(메모리 누수)을 극복하는 방법은 다음 포스팅에서 정리해보기로 한다.

자바스크립트는 어떻게 작동하는가: 메모리 관리 + 4가지 흔한 메모리 누수 대처법

profile
생각하는대로 살지 않으면, 사는대로 생각하게 된다.

0개의 댓글