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

최범수·2021년 10월 10일
13
post-thumbnail

Garbage Collection

쓸모 없어진 객체가 차지하는 메모리를 자동으로 해제하는 것

  • JavaScript, Python, Java 는 가비지 컬렉터가 자동으로 메모리를 관리해준다.
  • C, C++ 등 수동으로 메모리 관리하는 언어는 기본적으로 가비지 컬렉터가 없지만, 구현하여 사용할 수 있다.

장점

  • 메모리 관리를 완벽하게 할 필요는 없다.

단점

  • 언제 가비지 컬렉션이 진행될지 예측하기 어렵다.
    • 객체가 쓸모 없어지는 시점에 정확히 메모리가 해제되지 않기 때문에 최적의 메모리 관리가 되지 않는다.
  • 가비지 컬렉터가 동작하는 시간이 든다.
    • 어떤 객체가 쓸모 없는지 판단하는 시간이 소요된다.


자바스크립트 변수와 메모리

1. Primitive Type

  • 자바스크립트 원시 타입은 숫자, 문자열, 불리언, null, undefined, 심볼 이렇게 6개가 있다.
  • 원시 타입 데이터은 불변하는 데이터로, 메모리를 한번 할당받으면 값이 변경되지 않는다.
let num  // undefined
num = 80
num = 100
  • C에서는 변수에 값을 재할당하면, 변수가 가리키고 있는 메모리주소의 값이 바로 변경된다.
  • 그러나 자바스크립트에서는 값을 재할당하면 새로운 메모리 공간을 할당받아 값을 넣고, 변수가 가리키는 메모리주소를 변경한다.
  • 이전 값은 더 이상 사용되지 않으므로 가비지 컬렉션 대상이 된다.

2. Object Type

  • 원시 타입을 제외한 모든 것이 객체 타입으로, 객체, 배열, 함수 등이 모두 객체 타입
  • 객체 타입을 변수에 할당하면, 변수에는 실제 객체가 저장된 힙 메모리 주소가 저장된다.
const person = {
  name: 'Lee'
}
  • 객체 타입을 담은 변수를 다른 변수에 할당하면, 메모리 주소를 공유하기 때문에 같은 객체를 가리킨다.
const p1 = person

p1 === person  // true
  • 객체를 복사해서 새로운 변수에 할당하면, 객체의 구성은 같을 수 있어도 메모리 주소가 다르기 때문에 서로 다른 객체이다.
const p2 = { ...person }

p2 === pserson  // false
  • 즉, 자바스크립트 객체에 접근하는 방법은 메모리 주소를 통하는 것으로, 일종의 포인터 역할을 한다.
  • 객체를 가리키는 포인터가 하나도 없으면 객체를 사용할 방법이 없으므로 가비지 컬렉션의 대상이 된다.

중요한 점은 "메모리가 더 이상 필요하지 않을 때 해제해야 한다."



가비지 컬렉션 알고리즘

1. Reference-Counting

말 그대로 참조 개수를 카운팅하여 참조가 하나도 없으면 가비지로 판단하는 방법


예시

let x = {
  a: {
    b: 2
  }
}
  • 할당된 모든 메모리가 참조당하고 있는 상태로, 가비지가 없는 상태
  • 전역 변수는 가비지 컬렉션의 대상이 아니므로 x에 대한 카운팅을 제외

let y = x
x = 1
  • 변수 y에 변수 x를 대입하여 객체의 메모리 주소를 연결
  • 변수 x에는 1을 할당하여 객체와의 연결을 끊은 상태
  • 현재 모든 메모리의 참조가 아직 가비지가 없음

let z = y.a.b
y = 'bumsu'
  • 변수 z에 y.a.b를 대입하면 객체 b의 메모리 주소가 연결됨
  • 변수 y에는 문자열을 할당하여 객체 a와의 연결을 끊은 상태
  • 객체 a를 참조하는 변수가 하나도 없기 때문에 가비지로 인식하고 메모리 공간을 해제
z = null
  • 변수 z에 null을 대입하여 객체 b와의 연결을 끊은 상태
  • 객체 b가 가비지로 인식되어 메모리 공간이 해제되고, 그에 따라 숫자 2도 메모리 해제

순환 참조

  • 두개의 객체가 서로 참조하면 순환 구조가 생성되어, 가비지로 인식되지 않는다.
function f() {
	const x = {}
	const y = {}
	x.a = y
	y.a = x
}

f()
  • 선언 당시에는 x, y를 가리키는 참조가 없는 상태

  • 객체에 속성 a를 추가하고 서로를 참조하는 상태
  • 클로저와 같이 함수가 종료된 후에도 변수에 접근할 수 있는 형태는 아님
  • 함수 f의 컨텍스트가 끝나는 순간에 지역변수 x, y의 메모리를 해제시켜주어야 하는데, x.a, y.a에 의한 참조 카운팅이 되어 가비지로 분류할 수 없음

실제로 IE 6, 7버전에서는 Reference-Counting 방식으로 가비지 컬렉션을 진행했고 메모리 누수가 발생할 수 있었다고 함. 따라서 개발자가 순환 참조가 발생하지 않도록 신경쓰면서 개발하는 불편함이 있었다.

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


2. Mark-and-Sweep

이 알고리즘에서는 가비지를 "닿을 수 없는 메모리"로 정의.

roots 라는 전역변수의 집합부터 시작하여, roots가 참조하는 객체 → 그 자식들이 참조하는 객체 → ...

이런식으로 접근 가능한 객체들을 선별하고, 접근이 불가능한 객체들을 가비지로 판단하는 방법이다.

실행 컨텍스트가 소멸하는 순간, 접근하기 불가능한 객체가 되기 때문에 순환 참조는 발생하지 않는다.

2012년부터 모든 최신 브라우저들이 Mark-and-Sweep 알고리즘으로 가비지 컬렉션을 진행


Mark-and-Sweep의 3가지 상태

모든 객체를 3가지 상태로 분류하여 가비지 여부를 판단한다.

  1. White : 아직 가비지 컬렉터가 탐색하지 못한 상태
  2. Gray : 가비지 컬렉터가 탐색했으나, 해당 객체가 참조하는 객체들은 탐색하지 못한 상태
  3. Black : 가비지 컬렉터가 탐색했고, 해당 객체가 참조하는 객체들도 탐색 완료한 상태

Mark-and-Sweep의 구동 과정

Reference-Counting 알고리즘에서 사용한 예제를 통해 구동 과정을 살펴본다.

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

let y = x
x = 1

let z = y.a.b
y = 'bumsu'

z = null
  • 마지막 줄이 실행될 때까지 가비지 컬렉션이 수행되지 않았다고 가정하면, 이러한 메모리 구조가 된다.

  1. Marking
    1) roots를 모두 회색으로 마킹하고, Deque에 push한다.

    2) Deque에서 pop front하여 객체를 꺼내어 검은색으로 마킹한다. 3) 검은색으로 마킹된 객체가 참조하는 객체들을 회색으로 마킹하고, push front한다. 4) Deque가 완전히 빌 때까지 b, c를 반복한다.
    1. 최종적으로 검은색과 흰색으로 분류되고 Deque은 완전히 빈다.


  1. Sweep

    흰색으로 마킹된 객체들을 가비지로 인식하고 메모리를 해제한다.


  1. Compact

    메모리의 파편화가 심해지지 않도록 메모리를 재배치하여 메모리를 확보한다.


전체 동작과정



메모리 관점에서 가비지 컬렉션

자바스크립트 가비지 컬렉션이 실행되는 시점은 개발자가 특정할 수는 없다. 그런데 가비지 컬렉션은 SWT(Stop the World)가 발생하여 전체 프로그램이 멈추는 시간이 발생하고, 메모리 탐색을 하는 무거운 작업이므로 가비지 컬렉션이 실행되는 시점을 개념적으로라도 알고 있으면 좋다.

자바스크립트 가비지 컬렉션은 메모리가 일정 수준 이상으로 사용되면 실행되는데, 메모리 관점에서 동작 과정을 살펴보면서 언제 가비지 컬렉션이 실행되는지 알아본다.


1. V8엔진 메모리 구조

  • V8 엔진에서 힙 메모리는 객체와 동적 데이터가 저장되는 공간이자, 가비지 컬렉션이 발생하는 곳
  • 그 중에서도 New space, Old space 에서 일어난다.

New space (Young Generation)

  • 새로 만들어진 객체가 저장되는 공간
  • 아주 작은 크기가 할당되어 있고, 2개의 Semi space로 이루어져 있다. (To-space, From-space)
  • Scavenger라는 가비지 컬렉터가 관리하는 영역이고, Minor GC라고 한다.

Old space (Old Generation)

  • New space에서 살아남은 객체들이 저장되는 공간 (Minor GC가 2번이상 가비지 컬렉션을 실행해도 메모리 해제가 되지 않은 객체)
  • 포인터만 모아놓은 Old pointer space, 데이터만 모아놓은 Old data space로 나뉜다.
  • Major GC가 메모리를 관리한다.

2. 포인터와 데이터 분기

가비지 컬렉션 알고리즘을 구현하기 위해서는 참조 관계를 파악할 수 있어야 하고, 포인터와 데이터를 구분할 수 있어야 한다. 따라서, 자바스크립트도 메모리상에서 포인터와 데이터를 구분하는 방법을 채택했는데, 그 방법이 Tagged Pointers 방법이다.

데이터에 할당된 메모리 끝에 포인터인지 데이터인지 구분하는 비트 값을 저장한다. 약간의 메모리 오버헤드가 생기지만, 비교적 효율적으로 구현이 가능하다고 한다.


3. Minor GC 동작과정

  1. 새로운 객체로 인해 New space 공간이 초과되는 상황

    • New space는 To-spaceFrom-space로 나뉘어져 있는데, 객체는 To-space에만 저장한다.
    • 현재 새로운 객체가 들어오려고 하는데 To-space가 메모리 공간을 초과하게 되는 상황이다.

  1. To-space와 From-space의 역할 변경 및 객체 이동

    • From-space는 평소에 메모리가 전부 비어있다.
    • To-space가 가득차는 상황이 되면, 두 공간의 역할이 바뀐다.
    • From-space에 있는 객체들 중에 참조를 당하고 있는 객체들만 To-space로 이동 (가비지 컬렉션)
    • 참조가 끊어진 객체들은 제거한다.

  1. 새로운 객체는 메모리가 확보된 To-space에 추가된다.


  1. 다시 To-space 메모리가 초과되는 상황


  1. 2번 이상 살아남은 객체는 Old space로 이동

    • 1, 5번 객체는 이번 가비지 컬렉션에서 살아남았으므로 Old space로 이동한다.
    • 나머지 과정은 동일하다 .(역할 변경, 가비지 삭제, 새로운 객체 추가)

  1. 최종 결과

    • To-space, From-space 2개의 메모리 공간의 역할을 바꿔가며 가비지 컬렉션을 실행하는 방법을 스캐벤저(Scavenger)라고 한다.

    • New space 메모리 공간이 아주 작게 할당되어 있기 때문에, 자주 메모리 공간이 가득차고 가비지 컬렉션이 자주, 빠르게 일어난다.


4. Write Barriers

Minor GC도 Mark-and-Sweep 알고리즘처럼 루트에서 접근 불가능한 객체를 가비지로 판단한다. 그렇다면 New space 안에 있는 객체를 대상으로 가비지 컬렉션을 실행하더라도, 전체 힙 메모리를 다 검사해야 할 가능성이 있다. Minor GC는 자주 실행되므로, 매 실행마다 전체 힙 메모리를 검사하는 것은 아주 큰 오버헤드다.

그렇다고 Old space를 고려하지 않고 가비지 컬렉션을 실행하면 포인터만 살아있는 Dangling Pointer가 발생할 수 있다.


그래서 V8 엔진에서는 Write Barriers라는 것을 만들어서 전체 힙 메모리를 다 검사하는 것을 방지한다.

  • Old space에 있는 객체가 New space에 있는 객체를 참조할 때, Stored buffer라는 공간에 참조한 New space 객체의 위치를 저장한다.
  • Minor GC는 New space와 Stored buffer만 검사해도 객체들의 참조여부를 알 수 있다.

5. Major GC 동작과정

  • Major GC는 Mark-and-Sweep 알고리즘에서 설명한 것처럼 Marking-Sweep-Compact 과정으로 동작한다.
  • Old space가 충분하지 않을 때 Major GC가 가비지 컬렉션을 실행한다.
  • Minor GC, Major GC 모두 SWT를 발생시켜서 가비지 컬렉션 이외의 모든 동작이 멈춘다. Minor GC는 속도가 굉장히 빨라 성능에 영향이 적지만, Major GC는 메모리 공간이 커서 속도가 느리다. 따라서 Major GC에는 최적화 기법이 있다.

Chrome 64, Node.js v10에서는 GC가 mark하는 동안 앱이 멈추지 않습니다



References

JavaScript에서 메모리 누수의 원인 및 해결 방법

자바스크립트 메모리 누수와 해결 방법

자바스크립트의 메모리관리 - JavaScript | MDN

가비지 컬렉션

Trash talk: the Orinoco garbage collector

V8 Engine의 Garbage Collection

당신이 모르는 자바스크립트의 메모리 누수의 비밀

[Node.js] A tour of V8 : Garbage collection (번역)

원시 값과 객체의 비교

V8 엔진(자바스크립트, NodeJS, Deno, WebAssembly) 내부의 메모리 관리 시각화하기

profile
프론트엔드 개발자

0개의 댓글