2025. 6. 17. 09:00ㆍJavascript/Javascript
자바스크립트는 눈에 보이지 않는 곳에서 메모리 관리를 수행한다. 원시값, 객체, 함수 등 개발자가 코드로 작성하는 모든 것들은 메모리를 차지하는데 그렇다면 더이상 쓸모없어지게 된 것들은 어떻게 처리되는 걸까? 이 글에서는 자바스크립트 엔진이 어떻게 필요 없는 겂들을 찾아내 삭제하는지 알아보려 한다.
가비지 컬렉션 기준
자바스크립트는 도달 가능성(reachability)이라는 개념을 이용해 메모리 관리를 수행한다. 도달 가능한 값은 간단히 말해서 어떻게든 접근하거나 사용할 수 있는 값을 의미한다. 도달 가능한 값은 메모리에서 삭제되지 않는다.
아래의 값들은 태생적으로 도달할 수 있는 값이기 때문에 명백한 이유 없이는 삭제되지 않는다. 그리고 이러한 값들을 루트(root)라고 부른다.
- 현재 함수의 지역 변수와 매개변수
- 중첩 함수의 체인에 있는 함수에서 사용되는 변수와 매개변수
- 전역 변수
루트가 참조하는 값이나 체이닝으로 루트에서 참조할 수 있는 값 또한 도달 가능한 값이 된다. 예를 들어 전역 변수에 객체가 저장되어 있고 이 객체의 프로퍼티가 또 다른 객체를 참조하고 있다고 한다면 해당 객체의 프로퍼티가 참조하는 객체는 도달 가능한 값이 된다.
자바스크립트 엔진 내에서는 가비지 컬렉터(garbage collector)가 끊임없이 동작한다. 가비지 컬렉터는 모든 객체를 모니터링하고 도달할 수 없는 객체는 삭제한다.
let user = { // user엔 객체 참조 값이 저장됨
name: "John"
};
이 그림에서 화살표는 객체 참조를 나타낸다. 전역 변수 user는 {name: "John"}이라는 객체를 참조한다. 그러나 user의 값을 다른 값으로 덮어쓰면 참조가 사라진다.
user = null;
이제 {name: "John"}은 도달할 수 없는 상태가 되었다. {name: "John"}에 접근할 방법도, 참조하는 것도 모두 사라져버린 것이다. 그러면 가비지 컬렉터는 이를 메모리에서 삭제하는 것이다.
이번에는 참조를 user에서 admin으로 복사했다고 가정해 보자.
let user = {
name: "John"
};
let admin = user;
그리고 위에서와 같이 user의 값을 user = null;로 덮어쓴다면 어떻게 될까?
이 상황에서는 전역 변수 admin을 통해 여전히 객체에 접근할 수 있기 때문에 {name: "John"}은 메모리에서 삭제되지 않는다. 그러나 이 상태에서 다시 admin을 다른 값(null 등)으로 덮어쓰면 {name: "John"}은 메모리에서 삭제되는 것이다.
이번에는 가족관계를 나타내는 보다 복잡한 예시를 살펴 보자.
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;
삭제한 두 개의 참조 중 하나만 지웠다면 모든 객체가 여전히 도달 가능한 상태였을 것이다. 하지만 참조 두 개를 지우면 John으로 들어오는 참조(화살표)는 모두 사라져 John은 도달 가능한 상태에서 벗어나게 된다.
외부로 나가는 참조는 도달 가능한 상태에 영향을 주지 않는다. 외부에서 들어오는 참조만 도달 가능한 상태에 영향을 준다. 그러므로 John은 이제 도달 가능한 상태가 아니게 되어 메모리에서 제거된다. John에 저장된 데이터(프로퍼티) 역시 메모리에서 사라진다. 가비지 컬렉션의 작동 이후 메모리 구조는 아래와 같다.
도달할 수 없는 섬
객체들이 연결되어 섬 같은 구조를 만드는데 이 섬에 도달할 방법이 없는 경우 섬을 구성하는 객체 전부가 메모리에서 삭제된다. 이번에는 근원 객체 family가 아무것도 참조하지 않도록 해보자.
family = null
이제 메모리 내부 상태는 다음과 같아진다.
이 예시는 도달 가능성이라는 개념이 얼마나 중요한지 보여준다. John가 Ann은 여전히 서로를 참조하고 있고 두 객체 모두 외부에서 들어오는 참조를 갖고 있지만 이것만으로는 충분하지 않다는 걸 보여주는 것이다.
family 객체와 루트의 연결이 사라지면 루트 객체를 참조하는 것이 아무것도 없게 된다. 결국 섬 전체가 도달할 수 없는 상태가 되고 섬을 구성하는 객체 전부가 메모리에서 제거되어버린다.
가비지 컬렉션의 기본 알고리즘
mark and sweep이라 불리는 가비지 컬렉션의 기본 알고리즘에 대해 알아보자. 가비지 컬렉션은 대개 다음과 같은 단계를 거쳐 수행된다.
- 가비지 컬렉터는 루트(root) 정보를 수집하고 이를 기억(mark)한다.
- root가 참조하고 있는 모든 객체를 방문하고 다시 이것들을 mark한다.
- mark된 모든 객체에 방문하고 그 객체들이 참조하는 객체도 mark한다. 한 번 방문한 객체는 전부 mark하기 때문에 같은 객체를 다시 방문하는 일은 없다.
- root에서 도달 가능한 모든 객체를 방문할 때까지 위의 과정을 반복한다.
- mark되지 않은 모든 객체를 메모리에서 삭제한다.
아래와 같은 객체 구조가 있다고 가정해 보자.
오른쪽에 도달할 수 없는 섬이 있다. 이제 가비지 컬렉터의 mark-and-sweep 알고리즘이 이를 어떻게 처리하는지 살펴보자. 첫 번째 단계에서는 아래와 같이 root를 mark한다.
그리고 root가 참조하고 있는 것들을 mark한다.
도달 가능한 모든 객체를 방문할 때까지 mark한 객체가 참조하는 객체를 계속해서 mark한다.
방문할 수 없는 객체를 메모리에서 삭제한다.
지금까지 가비지 컬렉션이 어떻게 동작하는지에 대한 개념을 알아보았다. 자바스크립트 엔진은 실행에 영향을 미치지 않으면서 가비지 컬렉션을 더 빠르게 하는 다양한 최적화 기법을 적용한다.
- generational collection(세대별 수집): 객체를 새로운 객체와 오래된 객체로 나눈다. 객체 상당수는 생성 이후 제 역할을 빠르게 수행해 금방 쓸모가 없어지는데, 이러한 객체를 새로운 객체로 구분한다. 가비지 컬렉터는 이런 객체를 공격적으로 메모리에서 제거한다. 일정 시간 이상 살아남은 객체는 오래된 객체로 분류하고 가비지 컬렉터가 덜 감시한다.
- incremental collection(점진적 수집): 방문해야 할 객체가 많다면 모든 객체를 한 번에 방문하고 mark하는데 상당한 시간이 소모된다. 그러면 가비지 컬렉션에 많은 리소스가 사용되어 실행 속도 또한 느려질 것이다. 자바스크립트 엔진은 이러한 현상을 개선하기 위해 가비지 컬렉션을 여러 부분으로 분리한 다음 각 부분을 별도로 수행한다. 작업을 분리하고 변경 사항을 추적하는 데 추가 작업이 필요하긴 하지만긴 지연을 짧은 지연 여러 개로 분산시킬 수 있다는 장점이 있다.
- idle-time collection(유휴 시간 수집): 가비지 컬렉터는 실행에 주는 영향을 최소화하기 위해 CPU가 유휴 상태일 때에만 가비지 컬렉션을 실행한다.
'Javascript > Javascript' 카테고리의 다른 글
[Javascript] 객체: 생성자 함수 (1) | 2025.06.19 |
---|---|
[Javascript] async 함수의 병렬 처리 (1) | 2025.06.18 |
[Javascript] 대기 함수 만들기 (1) | 2025.06.16 |
[Javascript] 이벤트 한 번만 실행하기 (1) | 2025.06.13 |
[Javascript] 이벤트 제거하기 (0) | 2025.06.13 |