자바스크립트에서의 메모리 관리 (memory-management-in-javascript)
2020년 03월 20일
자바스크립트같은 동적 프로그래밍 언어에서 메모리 관리가 어떻게 되고 있는지 알고, 메모리 관리를 누가, 어느 시점에서 하는지에 대해서 정리해본다.

메모리

프로그램을 코드 덩어리라고 얘기한다면, 이 코드 덩어리가 실행되기 위해서는 분명 어딘가에 존재해야 한다. 이런 프로그램이 존재하는 곳이 메모리다. 프로그램은 메모리의 일부 영역을 빌려 자리잡고 프로그램이 실행된다.

운영체제는 다양한 영역의 메모리 공간을 프로그램에 준다. 일반적으로 4가지 영역으로 나뉜다. 코드영역, 데이터영역, 스택영역 그리고 영역이다. 컴파일 타입의 프로그래밍 언어에서는 컴파일 타임에 크기가 정해지는 변수들은 모두 스택 영역에 존재하고, 전역 변수 혹은 정적 변수는 데이터 영역에 존재하게 된다. 동적으로 할당되는 값의 경우 힙 영역에 존재하다 필요하면 쓰인다.

이런 메모리들은 변수들이 많아지고 사용하는 객체들이 쌓여가면서 메모리가 많아지게 된다. 메모리 영역이 많아지면 사용해야 할 변수들을 메모리 공간 안에서 찾아야하기 때문에 찾는 시간이 길어지므로 전체적인 프로그램 성능이 저하된다.

C언어같은 저수준 프로그래밍 언어에서는 개발자가 메모리를 할당하고 그 메모리가 더이상 사용하지 않는다고 판단되면 해제하는 코드를 직접 작성해야 한다. 하지만, 자바스크립트와 같은 고수준 프로그래밍 언어에서는 더이상 사용하지 않는 변수나 객체 등을 직접 찾아 메모리를 해제시켜주는 멋진 놈이 존재한다.

가비지 컬렉션

가비지 컬렉션은 자바스크립트와 같은 고수준 프로그래밍 언어에서 더이상 필요하지 않은 메모리 블록을 찾아 회수하는 목적을 가진 녀석이다. 저수준 언어에서는 '더이상 필요하지 않은'을 평가하는 주관이 개발자 자신이라면, 여기서는 가비지 컬렉션이 된다.

개발자는 사람이고, 가비지 컬렉션은 사람이 아니다. 그러므로 더이상 필요하지 않다고 결정하는 것이 부정확할 수도 있다. C언어 같은 경우 매우 세세한 부분까지 메모리에 할당시키고 해제하는 것이 컨트롤 가능하지만 자바스크립트에서는 비교적 어려울 수가 있다.

가비지 컬렉션의 한계

  1. 가비지 컬렉션은 프로그램이 동작하는 시간에 돌아간다. 자바스크립트에서는 코드가 실행되는 동안 계속 가비지 컬렉션이 일을 한다. 그러므로 성능 저하가 일어날 수 있다.
  2. 메모리 누수가 발생한다. 메모리 할당/해제에 관여할 수 있는 저수준 언어에서라면 메모리 누수를 개발자 스스로 제어할 수 있는 반면 가비지 컬렉션은 객체에 접근할 수만 있다면 '필요한 객체'라고 판단하기 때문이다.

자바스크립트에서의 가비지 컬렉션

자바스크립트에서 가비지 컬렉션은 '더이상 필요하지 않은 객체'는 '어떤 다른 객체도 참조하지 않는 객체'로 정의한다. 이 경우 가비지 컬렉션이 이 객체를 메모리에서 해제시킨다.

참조 횟수 알고리즘

let user = {
  name: {
    first: 'Kim',
    second: 'Monster'
  }
}

만약 위의 코드가 있다고 가정하면 이 코드는 가비지 컬렉션의 대상이 아니다. 가비지 컬렉션은 '어떤 곳에서도 참조되지 않는 객체'를 '더이상 사용되지 않는' 객체로 판단하기 때문이다. 위의 코드 같은 경우 아래에서 간단하게 참조할 수 있기 때문에 이 경우는 가비지 컬렉션의 대상이 되지 않는다.

자바스크립트는 현재 실행되고 있는 블록의 변수와 매개변수, 전역 변수, 중첩된 블록 내의 변수와 매개변수는 가비지 컬렉션의 대상이 되지 않는다.

또한 위의 블록에서 참조할 수 있는 모든 객체는 가비지 컬렉션의 대상이 아니다.

let user = {
  name: {
    first: 'Kim',
    second: 'Monster'
  }
}

let _u = user
user = ''

_u 변수에 user의 값을 참조 시켰다. 그런 뒤 본래 user 변수는 빈 문자열을 대입했다.

이 과정 역시 아직 user 객체는 가비지 컬렉션의 대상이 아니다. _u라고 하는 새로운 변수에 의해 참조되고 있기 때문에 메모리가 해제되지 않는다.

let _u = user
user = ''

let firstName = _u.name.first
_u = null

_u.name.first를 참조하는 새로운 변수 firstName을 만들었다. 그리고 이전에 만들었던 _u 변수에 null값을 대입했다. 이 과정 또한 가비지 컬렉션이 아무 일도 하지 않는다. firstName에서 아직도 객체를 참조하고 있기 때문이다.

let firstName = _u.name.first
_u = null

firstName = null

마지막으로, firstName의 값에 null을 대입했다. 이제 이름 값을 담고있던 객체를 어디에서도 참조할 수 없으므로 가비지 컬렉션이 회수해간다.

function doSomething() {
  let a = {}
  let b = {}

  a.c = b
  b.c = a

  return true
}

doSomething()

이 경우 a 객체와 b 객체가 서로 참조하기 때문에 순환 참조 현상이 일어난다. 서로 참조하고 있으면 이 객체는 어떤 곳에서든 참조가 가능하기 때문에 가비지 컬렉션의 회수 대상이 아니게 된다. 그러므로 이런 한계점이 존재한다.

Mark-and-sweep 알고리즘

이 알고리즘은 '더이상 필요없는 객체'를 '닿을 수 없는 객체'로 정의한다.

브라우저에서 자바스크립트는 전역 객체로 window 객체를 갖고, node.js 런타임에서는 global 객체를 전역 객체로 갖는다. 가비지 컬렉션은 주기적으로 이 객체들로 시작하여 닿을 수 없는 객체는 모두 정리해버린다.

이 경우 위의 알고리즘보다 더 효율적이라고 한다.

'더이상 필요없는 객체'는 모두 '닿을 수 없는 객체'지만 그 반대는 절대 존재할 수 없다.

순환 참조의 문제점도 이 알고리즘으로 해결되고, 현대적인 모든 브라우저가 이 알고리즘을 채택하고 있는 상황이라고 한다.

참고