최근에 면접에서 자바스크립트에서의 메모리 관리를 여쭤보셨는데, 잘 대답하지 못했던 기억이 있다.
이참에 정리해보고자 한다.
사실 모던 자바스크립트 개발에서 메모리 관리는 모를수도 있다고 생각한다. 면접에서 물어볼수도 있으니 아는게 좋음.
저수준 언어인 C언어 같은 경우 malloc, free 등의 메서드를 통해 메모리를 개발자가 관리해줘야 하지만 JS는 어차피 자바스크립트 엔진이 관리해주기 때문이다.
그럼에도 추후에 메모리 누수에 직면한다면 이를 해결하기 위해선 메모리 할당이 어떤 식으로 동작하는지에 대해 아는게 좋을 것이다.
자바스크립트에선 변수 혹은 함수 등 생각할 수 있는 모든 것들을 만들 때 JS엔진이 이에 대한 메모리를 할당하고 더 이상 필요하지 않으면 해제한다.
자바스크립트에서의 메모리 생명 주기는 다음과 같다.
메모리 할당
자바스크립트는 생성한 객체에 필요한 메모리를 할당한다.
메모리 사용
코드에서 명시적으로 수행되는 작업으로, 메모리를 읽고 쓰는 작업은 곧 변수에서 읽거나 변수에 쓰는 작업을 의미한다.
메모리 해제
이 단계는 JS 엔진에서도 처리된다. 할당된 메모리가 해제되면 새로운 용도로 사용할 수 있다.
이때의 '객체'는 메모리 관리의 맥락에서 자바스크립트의 객체 뿐 아니라 함수와 함수 스코프까지 포함한다.
그렇다면 자바스크립트에서의 메모리는 어디에 저장되는가?
JS 엔진엔 데이터를 저장할 수 있는 힙, 스택이 존재한다.
스택은 자바스크립트가 정적 데이터를 저장하는데 사용하는 자료 구조이다. 정적 데이터라 함은 엔진이 컴파일 타임에 크기를 알고 있는 데이터를 뜻하는데, 이는 원시값(string, boolean, number, bigint, null & undefined, symbol) 과 객체 및 함수를 가리키는 참조(Reference)가 이에 해당한다.
크기가 변경되지 않을 것임을 알고 있으므로, 엔진은 각 값에 대해 고정된 크기의 메모리를 사전에 할당한다.
그리고 이렇게 실행 직전에 메모리를 할당한다는 점에서, 이를 정적 메모리 할당이라고 한다.
힙은 자바스크립트가 객체와 함수를 저장하는 데이터를 저장하는 또 다른 공간이다.
스택과 달리, 고정된 크기의 메모리를 할당하지 않는다. 대신 필요에 따라 더 많은 공간을 할당한다.
이러한 방식으로 메모리를 할당하는 것을 동적 메모리 할당이라고 한다.
몇 가지 예시를 살펴보자.
const person = {
name: 'John',
age: 24,
}
JS는 힙에서 . 이객체에 대한 메모리를 할당한다. 실제 값은 원시 값이므로, 스택에 저장된다.
const hobbies = ['hiking', 'game']
배열도 객체이므로, 역시 힙에 저장된다.
let name = 'John';
const age = 24;
name = 'Micheal' // 메모리 새로 할당
const firstName = nameslice(0,4); // 메모리 새로 할당
원시 값은 불변(immutable)이다. 즉, JS는 원래 값을 변경하는 대신 새 값을 만든다.
모든 변수는 먼저 스택을 가리킨다. 원시 값이 아닌 경우 스택에는 힙의 객체에 대한 참조가 포함된다.
힙의 메모리는 특정한 방식으로 정렬되지 않으므로, 스택에 대한 참조를 유지해야 한다. 참조는 주소로, 힙의 객체는 이러한 주소가 가리키는 집으로 생각할 수 있다.
person과 newPerson 둘 모두 같은 객체를 가리키는 것을 확인할 수 있다.
마지막으로 메모리 해제에 대해서 알아보자.
메모리 해제는 메모리 할당과 마찬가지로 JS엔진, 정확히 가비지 컬렉터가 처리한다.
JS엔진이 어떤 변수나 함수가 더 이상 필요하지 않다는 것을 인식하게 되면, 이들이 차지하던 메모리를 해제한다.
다만 중요한 문제는, 메모리가 여전히 필요한지에 대한 여부를 결정할 수가 없다는 것이다. 다시 말해, 더 이상 메모리가 사용되지 않는 정확한 순간에 가비지 컬렉팅할 알고리즘은 존재할 수 없다.
그래도 일부 알고리즘은 이러한 문제에 대해 근사해를 제시한다.
이는 가장 쉬운 근사 알고리즘인데, 가리키는 참조가 없는 객체를 수집한다.
hobbies는 참조가 존재하는 객체이기 때문에, 마지막에 힙에서만 유지되고 있다는 점에 유의하자.
이 알고리즘의 문제는 '순환 참조'를 고려하지 않는다는 것이다. 하나 이상의 객체가 서로를 참조하지만, 더 이상 코드를 통해 접근할 수 없을 때 문제가 발생한다.
let son = {
name: 'John',
};
let dad = {
name: 'Johnson',
};
son.dad = dad;
dad.son = son;
son = null;
dad = null;
son
과 dad
객체가 서로를 참조하기 때문에, 알고리즘은 할당된 메모리를 해제하지 않는다. 더 이상 두 객체에 접근할 수 있는 방법도 없다.
null
로 할당하면 서로 참조가 존재하기 때문에, 알고리즘이 더 이상 사용할 수 없다고 인식하지 않게 된다.
Mark-and-Sweep 알고리즘에는 순환 참조에 대한 솔루션이 있다. 단순히 주어진 객체에 대한 참조를 계산하는 대신, 루트 객체에서 도달할 수 있는지를 감지한다.. 이때 루트
란 브라우저에서는 window 객체를, Node.js에서는 global 객체를 가리킨다.
알고리즘은 도달할 수 없는 객체를 쓰레기로 표시(mark)하고, 나중에 청소(sweep)한다. 루트 객체는 수집되지 않는다.
이렇게 하면 순환 참조가 더 이상 문제가 되지 않는다. 앞의 예시에서는 루트에서 dad
또는 son
객체에 도달할 수 없었다. 따라서 둘 다 쓰레기로 표시되고 수집된다.
2012년부터 이 알고리즘은 모든 최신 브라우저에서 구현된다. 이후로 성능과 구체적인 구현은 개선되었지만, 알고리즘의 핵심 아이디어 자체는 변하지 않았다.
자바스크립트의 메모리 관리의 핵심 개념을 요약해보았다.
메모리 관리가 자바스크립트를 통한 개발의 최우선적인 요소는 아닐 수 있지만, 그럼에도 JS에서의 메모리 관리가 동작하는 방식을 이해하고 있으면 도움이 될 것이다.