[JS] 메모리 관리 (가비지컬렉션)

DongDong·2022년 11월 15일
1

자바스크립트는 메모리 관리를 어떻게 할까 ?

let array = [1,2,3,4,5];
for(let i =0; i<10; i++){
	array = array.map( item => item);
}

위 예시처럼 map , reduce , filter 와 같은 원본 배열을 해치지 않고 새로운 배열을 리턴하는 자바스크립트 특정 함수들을 사용하면서

"새로운 배열을 생성하는 건 알겠는데 ,, 그럼 기존 메모리에 있던 배열은 어떻게 되는거지 ?"

라는 생각을 한 번쯤 해보신 적 있으신가요 ?

C언어와 같은 row level 언어에서는 메모리 관리를 해야합니다.
하지만 자바스크립트의 경우 자동으로 메모리를 할당하고 자동으로 해제합니다.
그렇다면 저희는 메모리 관리에 대해 신경쓰지 않아도 되는걸까요..?


저희는 메모리 관리에 대하여 low level 언어보다는 상대적으로 신경쓰지 않아도 되지만
메모리 관리에 대해 고민할 필요가 없다는 것은 아닙니다!

메모리의 생존주기

  • 필요할 때 할당한다.
  • 할당된 메모리를 사용한다.
  • 더 이상 필요하지 않을 때 해제한다.

보통 대부분의 프로그래밍 언어들에서 위의 생존주기를 따릅니다.

자바스크립트 입장에서의 "필요할 때 할당한다"는 아래와 같습니다.

  • 값을 초기화
	var char = 'a'; // 문자열을 담기 위한 메모리의 할당
    var num = 123;	// 숫자를 담기 위한 메모리의 할당
    var obj = {'a' : 1 , 'b' : 2 };	// 오브젝트와 오브젝트 안의 값을 담기위한 할당
	function func(){ ... }	// 자바스크립트에서 함수 또한 오브젝트이므로 위와 같음
	documentElement.addEventListener(...) // 위와 동일
  • 함수 호출을 통한 할당
	var date = new Date();	// Date 개체를 위한 메모리 할당
    var element = document.createElement(...)	// DOM element를 위한 메모리 할당
  • 특정 메소드의 사용
	var arr = ['a', 'b'];
	var arr2 = ['c', 'd'];
	var arr3 = arr.concat(arr2);
	// arr 와 arr2 를 이어붙여, 4개의 원소를 가지는 새로운 메모리를 가지는 배열 arr3

"할당된 메모리를 사용한다" 부분은 거의 모든 프로그래밍 언어들이 동일합니다.
따로 다루지는 않겠습니다.

이제 대망의 "더 이상 필요하지 않을 때 해제한다." 부분을 들여다 보겠습니다.
대부분의 문제가 이 단계에서 발생합니다.
우리는 더 이상 필요하지 않을 때를 모르기 때문이죠.
low level의 언어는 개발자가 의도적으로 메모리를 할당하고 해제합니다.

하지만 고수준의 언어인 자바스크립트는 가비지 콜렉션이라는 자동으로 메모리를 관리해주는 방법을 사용합니다.
가비지 콜렉션은 메모리 할당을 추적하고 할당된 메모리가 더 이상 사용되지 않는지 판단하여 회수하는 역할을 수행합니다.

그렇다면 가비지 콜렉션은 어떻게 메모리가 더 이상 사용되지 않는지 알까요 ?

가비지 콜렉션은 참조(Reference) 를 통해 판단하며
아무도 참조하지 않는 메모리를 추적하여 해당 메모리를 해제시키는 것입니다.

A 라는 메모리를 통해 B 라는 메모리에 접근할 수 있다면 B는 A에 "참조"된다.

참조하는 다른 오브젝트가 존재하지 않는 경우 수집이 가능하며 이러한 오브젝트를 가비지 라고 합니다.
위에서 말한 것과 같이 참조를 통해 판단하여 메모리 할당을 해제하는 가비지 콜렉션의 알고리즘을
참조-세기(Reference-counting) 가비지 콜렉션 이라고 합니다.

참조-세기(Reference-counting) 가비지 콜렉션

var x = {
		a : {
    		b : 2
    	}
	}
    
// 2 개의 오브젝트가 생성되었고 b는 a라는 오브젝트의 속성으로 참조되었고
// a는 x라는 변수에 할당되었습니다. 참조 관계가 분명하기 때문에 가비지 콜렉션은 수행되지 않습니다.

var y = x;
// 'y' 변수는 위의 오브젝트를 참조하는 두 번째 변수입니다.

x = 1;
// 이제 'y' 변수가 위의 오브젝트를 참조하는 유일한 변수가 되었습니다.

var z = y.a;    
// 위의 오브젝트의 'a' 속성을 참조했습니다.
// 이제 'y.a'는 두 개의 참조를 가집니다.
// 'y'가 속성으로 참조하고 'z'라는 변수가 참조합니다.

y = "mozilla";
// 참조하는 유일한 변수였던 y에 다른 값을 대입했습니다
// 아직은 'z' 변수에 의해 오브젝트의 a 속성이 참조되므로 메모리를 해제할 수 없습니다.

z = null;       
// 'z' 변수에 다른 값을 할당했습니다.
// 이제 맨 처음 'x' 변수가 참조했던 오브젝트를 참조하는
// 다른 변수는 없으므로 가비지 콜렉션이 수행됩니다.

참조-세기(Reference-counting) 가비지 콜렉션 은 위와 같이 동작하며
해당 알고리즘은 순환 참조라는 명백한 한계를 가지고 있습니다.

// 순환 참조 예시
function f() {
  var x = {};
  var y = {};
  x.a = y;         // x는 y를 참조합니다.
  y.a = x;         // y는 x를 참조합니다.

  return null;
}

f();

함수 호출이 완료되어 null을 return하게 되면 이 두 객체는 스코프를 벗어나게 되고 그 시점에서 두 객체는 불필요해지므로 할당된 메모리는 회수되어야 하지만
두 객체가 서로를 참조하고 있으므로, 참조-세기 알고리즘은 둘 다 가비지 컬렉션의 대상으로 인지하지 못합니다.
이러한 순환 참조는 메모리 누수의 흔한 원인입니다.

그렇다면 아무도 참조하지 않는 오브젝트 말고 접근할 수 없는 오브젝트로 판단하면 어떨까요?
접근할 수 없는 오브젝트로 판단하게 된다면 위 예제에서 스코프를 벗어나게 되면 더 이상 접근할 수 없는 오브젝트이기 때문에 순환 참조로 인해서 메모리 누수가 되는 일이 없을 것 같지 않나요?

이러한 아이디어를 통해 나온 알고리즘이
표시하고-쓸기(Mark-and-sweep) 알고리즘 이라고 합니다.
이 알고리즘은 "더 이상 필요없는 오브젝트"를 "닿을 수 없는 오브젝트"로 정의합니다.

표시하고-쓸기(Mark-and-sweep) 알고리즘

해당 알고리즘은 루트라는 오브젝트의 집합(전역 변수들)을 가지고 있습니다.
주기적으로 가비지 콜렉터는 루트에서부터 참조 값을 토대로 뻗어나가며
"닿을 수 없는 오브젝트" 를 찾아냅니다.
그리고 닿을 수 없는 오브젝트에 대하여 가비지 콜렉션을 수행합니다.

표시하고-쓸기(Mark-and-sweep) 알고리즘은 2012년을 기준으로 모든 최신 브라우저들이 사용하는 가비지 콜렉션 알고리즘이 되었습니다.

지금 저희가 사용하고 있는 웹 브라우저들은 모두
루트가 참조하고 있는 오브젝트 -> 이 오브젝트가 참조하고 있는 오브젝트 -> ... 을 진행하며
도달할 수 없는 오브젝트들을 가비지 콜렉션의 대상으로 선정하고 메모리를 해제하는 것입니다.

하지만 이 또한 한계점이 있는데,

어떤 메모리를 언제 해제할지에 대해 수동으로 결정하는 것이 편리하고 좋을 때가 있지만
현재 자바스크립트에서는 명시적으로 혹은 프로그래밍 방식으로 가비지 컬렉션을 작동할 수 없다고 합니다.


참조
https://developer.mozilla.org/ko/docs/Web/JavaScript/Memory_Management
흠 ,, 위 링크와의 참조 관계가 분명하니 가비지 콜렉션은 동작하지 않겠네요 ,,?

profile
중요한건 꺾이지 않는 마음

0개의 댓글