자바스크립트의 메모리 관리는 우리에게는 보이지 않게 자동으로 실행됩니다. 우리가 원시타입의 변수나 혹은 객체, 함수를 선언할때도 모두 메모리를 사용합니다. 만약에 이러한 것들이 더이상 필요없게 된다면? 자바스크립트 엔진은 어떻게 이것들을 찾아내어 삭제할까요?
접근 가능성(Reachability)
자바스크립트 메모리 관리의 주요 개념은 접근 가능성입니다. 간단하게 말하면 “접근 가능한” 값은 어떻게든 엑세스가 가능하거나 사용할 수 있는 값임을 뜻합니다. 이들은 메모리에 유지되는것을 보장 받습니다.
- 명백한 이유로 삭제될 수 없는 본질적으로 값에 접근 가능한 기본 세트가 있습니다. 이런것들을 뿌리(Root)라고 부르겠습니다.
- 현재 함수의 지역 변수와 매개변수
- 다른 함수의 중첩 호출로 실행된 함수의 경우 현재의 스코프 체인으로 접근 가능한 변수와 매개변수
- 전역 변수
- (내부적으로 구현된 다른 것들도 있다;;)
- 기타 다른 값은 참조(Reference) 또는 레퍼런스의 참조(A chain of references)에 의해 루트에서 도달 가능한 것으로 간주합니다. 예를 들어 로컬 변수가 특정 객체를 참조하고 있고 그 객체가 또다른 객체를 참조하는 프로퍼티를 가지고 있다면 그 객체에 도달 가능하다라고 간주합니다. 그리고 그것들이 참조하는 다른 것들도 도달할 수 있습니다.
자바스크립트 엔진의 백그라운드 프로세스로 동작하는 가비지 콜렉터라는것이 있습니다. 이것은 모든 객체들을 모니터링 하며 그것들이 접근 불가능하게 되었을 때 삭제하는 작업을 수행합니다.
간단한 예제
여기에 아주 간단한 예제가 있습니다.
// user는 객체에 대한 참조를 가지고 있습니다. let user = { name: "John" };
여기에 표시된 화살표는 객체 참조를 나타냅니다. 전역 변수 “user”는 {name: “John”} 객체를 참조합니다. (여기서 우리는 짧게 존이라고 부르겠습니다). 존의 “name” 프로퍼티는 원시 타입을 저장하여 객체의 내부에 위치합니다. 여기에서 우리가 user의 값을 덮어쓰게 되면 참조를 잃게 됩니다.
user = null;
이제 존은 접근이 불가능하게 되었습니다. 여기에 접근할 방법은 없으며 아무도 존을 참조하지 않게 되었습니다. 가비지 콜렉터는 데이터를 회수하고 메모리를 비우게 됩니다.
두개의 참조
이번에는 user를 admin으로 참조를 복제하였다고 상상 해 보겠습니다.
let user = { name: "John" }; let admin = user;
이제 우리는 똑같은 작업을 한번 더 해보겠습니다.
user = null;
하지만 여전히 admin 변수가 존을 참조하고 있으므로 메모리에 유지됩니다. 우리가 admin 도 다시 덮어쓴다면 그때 삭제 될 것입니다.
상호 연결된 객체
이번엔 좀 더 복잡한 예제를 살펴보겠습니다. 가족을 예로 들어보겠습니다.
function marry(man, woman) { woman.husband = man; man.wife = woman; return { father: man, mother: woman } } let family = marry({ name: "John" }, { name: "Ann" });
marry는 두개의 객체를 서로 참조하게 하고 이 둘을 참조하고 있는 새로운 객체를 반환하는 “결혼”(?)을 시키는 함수입니다. 메모리 구조의 결과는 다음과 같습니다.
모든 객체는 서로 접근 가능하게 되었습니다. 이제 두개의 참조를 삭제해보겠습니다.
delete family.father; delete family.mother.husband;
이 두개의 참조중에 하나만 삭제할 경우에는 여전히 모든 객체가 접근 가능하므로 객체가 삭제되기에 충분하지 않습니다. 하지만 둘 모두를 삭제할 경우 존을 참조하는 참조는 더이상 존재하지 않게 됩니다.
존이 여전히 가지고 있는 바깥으로 향하는 참조는 상관 없습니다. 오로지 안으로 들어오는 참조만이 존을 참조가능한 상태로 만들어줍니다. 그러므로 존은 이제 접근 불가능하게 되었고 메모리에서 제거되게 될 것입니다. 가비지 콜렉션이 동작한 이후에는 다음과 같이 됩니다.
접근 불가능한 섬
외부에서 접근 불가능한, 자기들끼리만 상호 참조하여 만들어진 완벽한 형태의 섬도 메모리에서 삭제 가능합니다. 소스코드는 위와 동일하다고 가정하고 다음의 코드를 실행하도록 하겠습니다.
이 예제는 접근 가능성에 대한 매우 중요한 개념을 보여주는 데모입니다. 명백하게 존과 앤은 연결되어있습니다. 그 둘은 안/밖으로 연결되는 링크들 모두를 가지고 있습니다. 하지만 이것만으로는 충분하지 않습니다. 이전의 family 객체는 루트(Root)와의 연결이 끊어지게 되었습니다. 그러므로 이 완벽한 섬은 접근 불가능하게 되었으며 삭제될 것입니다.
내부 알고리즘
기본적인 가비지 콜렉션의 알고리즘은 마크 앤 스윕(Mark-and-sweep) 이라고 불립니다. 일반적으로 가비지 콜렉션은 다음의 과정을 거칩니다.
- 가비지 콜렉터는 루트를 획득하여 그들을 마크(기억)합니다.
- 그리고 그들이 참조하고 있는 모든 것들에 방문하여 마크합니다.
- 그리고 마크한 모든 객체에 방문하여 그들의 참조 역시 마크합니다. 모든 객체들을 기억하고 나면 미래에는 같은 객체를 두번 방문하지 않습니다.
- 루트로부터 접근 가능한 방문하지 않은 참조가 있다면 계속해서 반복합니다.
- 마크되지 않은 모든 객체는 삭제됩니다.
예를 들어 다음과 같은 객체의 구조가 있다고 해보겠습니다.
우리는 오른편에 “접근 불가능한 섬”을 발견할 수 있습니다. 이제 가비지 콜렉터가 진행하는 마크 앤 스윕 과정이 이것을 어떻게 다루는지 보겠습니다. 다음은 루트로부터 첫번째 과정을 거친 결과입니다 :
이후에 그들의 참조들도 마크합니다 :
그리고 그들의 참조도 반복합니다. 가능할때까지 :
이제 방문할 수 없는 객체들은 접근 불가능한것으로 간주되어 삭제될 것입니다.
이것이 가비지 콜렉터가 동작하는 개념입니다. 자바스크립트 엔진은 어플리케이션의 실행에 영향을 주지 않고 빠르게 수행되도록 하기 위해 많은 최적화 옵션을 적용하고 있습니다.
- 세대별 수집 – 객체는 “새로운 객체”와 이전 객체” 두개의 세트로 나뉘어집니다. 많은 객체들은 나타나고 그들의 일을 수행하고 빨리 죽습니다. 이것들은 공격적으로 청소될 수 있을 것입니다. 하지만 충분히 오래 살아남은 객체들은 “오래된” 객체가 되어 덜 자주 검사를 받게 됩니다.
- 증분 수집 – 많은 객체가 있고 이러한 많은 객체를 한번에 전부다 방문하며 마크하는 과정을 거치게 되면 실행에 눈에 띄는 지연이 발생할 수 있습니다. 그래서 엔진은 이러한 수거 작업을 여러 조각으로 나누어 수행합니다. 이렇게 나눈 조각의 변화를 추적할 수 있도록 부가적인 처리가 필요하지만 큰 한번의 딜레이 대신에 짧은 단위의 딜레이를 가질 수 있습니다.
- 유휴 시간 수집 – 가비지 콜렉터는 CPU가 유휴상태일 때만 실행되어 어플리케이션의 실행에 끼치는 영향을 줄입니다.
이것 말고도 가비지 콜렉터의 다른 최적화 기능들이 있습니다. 각 엔진들이 구현하고 있는 추가적인 테크닉들이 있으며 엔진의 발전에 따라 지속적으로 변화하고 있습니다.
정리
알아야 하는 핵심은 다음과 같습니다.
- 가비지 콜렉션은 자동으로 실행됩니다. 우리는 이것을 강제로 실행하거나 막을 수 없습니다.
- 객체는 그들이 접근 가능한 동안 메모리에 유지됩니다.
- 참조가 된다는 것이 루트(Root)에서 참조 가능한것과 같은 말은 아닙니다 : 상호 참조하고 있는 객체들이 전체에서 보면 참조 불가능할 수 있습니다.
참고 : https://javascript.info/garbage-collection