자바스크립트의 가비지 컬렉션에 대해 알고 있나요?
면접용
가비지 컬렉션은 더 이상 사용되지 않는 메모리를 자동으로 해제하는 과정입니다.
자바스크립트 엔진은 자동으로 가비지 컬렉터가 동작하고 있으며, 도달 가능성 개념을 사용하여 메모리를 관리합니다.
가비지 컬렉션은 mark-and-sweep 알고리즘을 사용하여 root 객체에서부터 참조 가능한 모든 객체를 방문하고, 도달할 수 없는 객체는 메모리를 회수합니다. 이를 통해 순환 참조 문제를 해결할 수 있습니다.
꼬리 질문: 클로저가 참조하는 객체는 가비지 컬렉션에 의해 자동으로 해제되나요?
클로저가 외부 변수를 더 이상 참조하지 않으면 자동으로 해제됩니다. 하지만 계속 참조하고 있다면 가비지 컬렉터가 수거할 수 없습니다. 이 경우에는 해당 변수를 null로 설정하거나 클로저를 제거하는 등의 방법으로 불필요한 참조를 끊을 수 있습니다.
개념설명
CS에서의 가비지 컬렉션
프로그램이 실행될 때 데이터는 메모리의 특정 공간에 할당됩니다. 대부분의 프로그래밍 언어에서 메모리 생존 주기는 다음과 같습니다.
필요할 때 메모리를 할당합니다.
값 선언, 함수 호출, 객체 생성 등 여러 상황에서 자동으로 할당됩니다.
js 예) 변수 x와 값 10이 메모리 공간에 저장됩니다
// 값이나 변수면 stack에 저장 // 객체면 heap에 저장 let x = 10;
할당된 메모리를 사용합니다.
변수, 객체의 값을 읽고 쓰거나 함수에 인자를 전달하는 행동이 이에 해당합니다.
더 이상 필요하지 않으면 메모리를 해제합니다.
이 주기에서 가비지 컬렉션은 3번 과정에 해당합니다. 즉, 가비지 컬렉션은 필요 없는 객체에 할당된 메모리를 해제하는 작업을 뜻합니다.
JavaScript, Python, Java 등의 고수준 언어는 가비지 컬렉터가 존재하여 백그라운드에서 자동으로 메모리를 관리해줍니다. 반면 수동으로 메모리를 관리하는 저수준 언어(c, cpp)에는 없어서 따로 구현하여 사용합니다.
이는 장단점이 있습니다. 가비지 컬렉터가 있을 경우 개발자는 가비지 수집 과정에 대해 직접적으로 접근할 수 없습니다.
자바스크립트에서의 가비지 컬렉션
자바스크립트 엔진 내에서는 가비지 컬렉터가 끊임없이 자동으로 동작하며, 모든 객체를 모니터링합니다. 할당된 메모리가 필요하지 않은 시점을 확인하고 이를 회수합니다.
그럼 자바스크립트에서는 무엇을 기준으로 할당된 메모리가 필요하지 않다고 판단할까요?
자바스크립트는 도달 가능성(reachability)
이라는 개념을 사용해 메모리 관리를 수행합니다. 도달 가능한 값은 메모리에서 삭제하지 않고, 도달할 수 없는 객체는 삭제하는 방식입니다.
여기서 도달 가능한 값이란 root 값과 그와 연결된 객체들을 뜻합니다. 가비지 컬렉션은 root 객체에서 시작하여 참조된 객체들을 따라갑니다.
root 값에는 다음 값들이 해당합니다.
전역 객체 (window 또는 global)
현재 함수의 지역 변수, 매개변수 (활성화된 함수 스코프)
전역 변수
클로저 (내부 함수가 외부 함수의 변수에 접근할 수 있는 경우)
전역 변수나 객체를 자주 사용하는 것이 좋지 않은 이유
메모리 관리 측면에서, 전역 객체는 어디에서든 참조될 수 있습니다.
가비지 컬렉터는 이를 참조하는 모든 변수가 존재할 때까지 해제하지 않기 때문에, 더 이상 필요하지 않은 객체가 메모리에서 해제되지 않고 계속 남게 됩니다. ⇒ 메모리 누수
또한 가비지 컬렉션은 CPU 자원을 소모하는 작업이므로 성능에도 영향을 미칠 수 있습니다.
도달 가능성에 대한 예시를 살펴봅시다. user
라는 전역 변수가 있고, 해당 변수는 { name: “John” }
객체를 참조하고 있습니다. root 값과 그와 연결된 객체이므로 둘 다 도달 가능한 값입니다.
let user = {
name: "John"
};
만약 user의 값을 다른 값으로 덮어쓰면 기존 객체를 더 이상 참조하지 않습니다. 이 경우 { name: “John” } 에 접근할 방법이 없기 때문에 도달할 수 없는 상태가 됩니다.
그럼 가비지 컬렉션은 해당 객체에 저장된 데이터와 메모리를 삭제하게 됩니다.
user = null;
가비지 컬렉션의 알고리즘
가비지 컬렉션은 도달 가능성을 계산하기 위해 다양한 알고리즘을 사용합니다.
이전에는 Reference-counting 알고리즘을 사용했습니다. 이 알고리즘은 어떤 다른 객체도 참조하지 않는 객체(garbage)를 더 이상 필요 없는 객체라고 여겨 삭제합니다.
하지만 위 알고리즘은 순환 참조
를 다룰 때 문제가 발생합니다.
function testRef() {
const x = {};
const y = {};
x.a = y; // x는 y를 참조
y.a = x; // y는 x를 참조
return "problem";
}
testRef();
위 예제에서는 두 객체가 서로 참조하는 속성으로 생성되어 순환 구조를 이루고 있습니다.
함수 호출 시작 시 두 객체에게 할당된 메모리는 호출이 완료되면 스코프를 벗어나 불필요해지고, 따라서 가비지 콜렉터에게 회수되어야 합니다.
하지만 두 객체가 서로를 참조하고 있으므로 Reference-counting 알고리즘의 조건에 따라 garbage로 인식할 수 없습니다.
이렇게 순환 참조 상황에서 메모리 누수가 발생할 수 있다는 문제점이 있습니다.
따라서 좀 더 개선된 형태의 mark-and-sweep 알고리즘이 등장했습니다. 이 알고리즘은 garbage를 “도달할 수 없는 객체”로 정의합니다.
앞에서 말한 예시와 같으며, 현재 모든 최신 엔진이 제공하는 알고리즘입니다.
알고리즘의 진행 과정은 다음과 같습니다.
가비지 컬렉터는 root 값을 수집하고 이를 mark(기억)합니다.
root에서 도달 가능한 모든 객체를 방문할 때까지 다음 과정을 반복합니다.
root가 참조하고 있는 모든 객체를 방문하고 이를 mark합니다.
mark된 모든 객체에 방문하고, 그 객체들이 참조하는 객체도 mark합니다. (중복 방문하지 않습니다.)
mark되지 않은 모든 객체를 메모리에서 삭제합니다.
이러한 과정을 거치면 단순히 참조한다고 해서 도달 가능하게 되지 않기 때문에 순환 참조 문제를 해결할 수 있습니다.
위의 예시에 이 알고리즘을 적용하면, 함수 호출 완료 후 2개의 객체들은 더 이상 전역 객체(root)에서 참조하고 있는 어떤 객체에서도 참조하고 있지 않기 때문에 삭제할 수 있습니다.
클로저와 가비지 컬렉션의 관계
클로저는 함수가 선언된 시점의 변수를 기억하고, 그 변수를 참조합니다. 이때 메모리를 계속해서 참조하기 때문에 가비지 컬렉션이 클로저를 해제하지 못할 수 있습니다. 이는 곧 메모리 누수로 이어집니다.
따라서 클로저를 사용할 때는 불필요한 참조를 제거해야 합니다. 클로저 내부에서 더 이상 사용하지 않는 변수를 null로 설정하여 해제할 수 있습니다.
function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
// 더 이상 counter가 필요 없으면, 클로저 내부의 변수 count를 null로 설정하여 해제합니다.
counter = null;
// 이제 메모리에서 해당 참조가 해제되었고, count는 가비지 컬렉터에 의해 회수될 수 있습니다.
Last updated