JS는 보이지 않는 곳에서 메모리 관리를 수행한다.
원시값, 객체, 함수 등 우리가 만드는 모든 것은 메모리를 차지하는데, 더는 쓸모
없어지게 되느 것들을 어떻게 처리할까? JS엔진이 어떻게 찾아내 삭제하는지 알아보자
가비지 컬렉션 기준
JS는 도달 가능성(reachability)라는 개념을 사용해 메모리 관리를 수행합니다.
이는 쉽게 말해 어떻게든 접근하거나 사용할 수 있는 값을 의미.
도달 가능한 값은 메모리에서 삭제되지 않습니다.
- 이 값들은 태생부터 도달 가능하기 때문에, 명백한 이유 없이는 삭제되지 않습니다.
예시:
- 현재 함수의 지역 변수와 매개변수
- 중첩 함수의 체인에 있는 함수에서 사용되는 변수와 매개변수
- 전역 변수
- 기타 등등
이런 값은 '루트'라고 부릅니다. - 루트가 참조하는 값이나 체이닝으로 루트에서 참조할 수 있는 값은 도달 가능한 값이 됩니다.
전역 변수에 객체가 저장되어있다고 가정해보자. 이 객체의 프로퍼티가 또 다른 객체를 참조하고 있다면,
프로퍼티가 참조하는 객체는 도달 가능한 값이 됩니다. 이 객체가 참조하는 다른 모든 것들도 도달
가능하다고 여겨진다.
JS엔진 내에서 이 가비지 컬렉터가 끊임없이 동작한다. 이는 모든 객체를 모니터링하고, 도달할 수 없는 객체는 삭제한다.
간단한 예시
// user엔 객체 참조 값이 저장됩니다.
let user = {
name: "John"
};
이 코드에서 전역 변수 "user"는 {name: "john"} 이라는 객체를 참고한다. john의 프로퍼티 "name"은
원시값을 저장하고 있기 때문에 객체 안에 표현했습니다.
user = null;
그런데 이를 다른 값으로 덮어씌우면 참조가 사라진다.
이제 john은 도달할 수 없는 상태가 되었습니다. john에 접근할 방법도, john을 참조하는 것도 모두 사라졌다.
이제 가비지 컬렉터는 이제 john에 저장된 데이터를 삭제하고, john을 메모리에서 삭제합니다.
만약 admin 이라는 전역변수가 user의 객체 참조값을 복사한 후 위와 같은 방식으로
user에 null 값을 넣더라도 admin 에서는 여전히 참조한 값에 접근할 수 있기에 메모리에서 삭제하지 않는다.
연결된 객체
가족 관계를 나타내는 복잡한 예시를 살펴보자
function marry(man, woman) {
woman.husband = man;
man.wife = woman;
return {
father: man,
mother: woman
}
}
let family = marry({
name: "John"
}, {
name: "Ann"
});
지금은 모든 객체가 도달 가능한 상태지만 여기서 참조 두개를 지워보면
delete family.father;
delete family.mother.husband;
이 중 하나만 삭제했다면 모든 객체가 여전히 도달 가능한 상태지만
john으로 들어오는 참조는 모두 사라져서 john은 도달 가능한 상태에서 벗어난다.
외부에서 들어오는 참조만이 도달 가능한 상태에 영향을 준다. john은 이제 도달 가능한
상태가 아니기 때문에 메모리에서 제거된다. john에 저장된 데이터 (프로퍼티) 역시 메모리에서 삭제된다.
도달할 수 없는 섬
객체들이 연결되어 섬 같은 구조를 만드는데, 이 섬에 도달할 방법이 없는 경우,
섬을 구성하는 객체 전부가 메모리에서 삭제된다.
근원 객체 family가 아무것도 참조하지 않도록 해보자
family = null;
john과 Ann은 여전히 서로를 참조하고 있고, 두 객체 모두 외부에서 들어오는 참조를 갖고 있지만,
이것만으로는 충분하지 않다는 것을 보여준다.
"family" 객ㄱ체와 루트의 연결이 사라지면 루트 객체를 참조하는 것이 아무것도 없어지게 된다.
섬 전체가 도달할 수 없는 상태가 되고, 섬을 구성하는 객체 전부가 메모리에서 제거된다.
내부 알고리즘
'mark-and-sweep'이라 불리는 가비지 컬렉터에 기본 알고리즘에 대해 알아보자ㅣ.
대개 다음 단계를 거쳐 수행된다.
- 가비지 컬렉터는 루트정보를 수집하고 이를 'mark(기억)'합니다.
- 루트가 참조하고 있는 몯느 객체를 방문하고 이를 'mark'합니다.
- mark된 모든 객체에 방문하고 그 객체들이 참조하는 객체도 mark합니다.
한번 방문한 객체는 전부 mark하기 때문에 같은 객체를 다시 방문하는 일은 없습니다. - 루트에서 도달 가능한 모든 객체를 방문할 때까지 위 과정을 반복
- mark되지 않는 모든 객체를 메모리에서 삭제합니다.
루트에서 페인트를 들이붓는다고 상상하면 과정에 이해가 쉽다. 루트를 시작으로 참조를 따라가면서
도달가능한 객체 모두에 페인트가 칠해진다고 생각하면 된다. 이 때 페인트가 묻지 않는 객체는 메모리에서 삭제된다.
JS엔진은 실행에 영향을 미치지 않으면서 가비지 컬렉션을 더 빠르게 하는 다양한 최적화 기법을 적용한다.
최적화 기법:
- generational collection(세대별 수집) - 객체를 '새로운 객체'와 '오래된 객체'로 나눈다. 객체 상당수는
생성 이후 제 역할을 빠르게 수행해 금방 쓸모가 없어지는데, 이런 객체를 '새로운 객체'로 구분. 가비지 컬렉터는
이런 객체를 공격적으로 메모리에서 제거한다. 일정 시간 이상 동안 살아남은 객체는 '오래된 객체'로 분류하고 덜 감시한다. - incremental collection(점진적 수집) - 방문해야 할 객체가 많다면 모든 객체를 한 번에 방문하고 mark하는데
상당히 시간이 소모된다. 가비지 컬렉션에서 많은 리소스가 사용되어 실행 속도도 눈에 띄게 느려질 것. JS엔진은 이런
현상을 개선하기 위해 가비지 컬렉션을 여러 부분으로 분리한 다음, 각 부분을 별도로 수행한다. 작업을 분리하고, 변경 사항을
추적하는 데 추가 작업이 필요하긴 하지만 긴 지연을 짧은 지연 여러개로 분산시킬 수 있는 장점이 있다. - idle-time collection(유휴 시간 수집) - 가비지 컬렉터는 실행에 주는 영향을 최소화하기 위해 CPU가 유휴상태일 떄에만 가비지 컬렉션을 실행한다.
'JavaScript > Basics' 카테고리의 다른 글
[JavaScript] new 연산자와 생성자 함수 (0) | 2022.03.11 |
---|---|
[JavaScript] 메서드와 this (0) | 2022.03.11 |
[JavaScript] 참조에 의한 객체 복사 (0) | 2022.03.11 |
[JavaScript] 폴리필 - Polyfills and transpilers (0) | 2022.03.10 |
[JavaScript] 테스트 자동화 (0) | 2022.03.10 |