개요
Chrome에서 메모리 누수를 디버깅하는 것은 훨씬 쉬워졌습니다. Chrome의 DevTools는 이제 C ++ DOM 객체를 추적하고 스냅 샷을 작성하고 JavaScript를 통해 참조 가능한 모든 DOM 객체를 표시합니다. 이 기능은 V8 가비지 수집기의 새로운 C ++ 추적 메커니즘의 이점 중 하나입니다.
배경
가비지 콜렉션 시스템에서의 메모리 누수는 다른 오브젝트로부터 의도하지 않은 참조로 인해 사용되지 않은 오브젝트가 해제되지 않은 경우 발생합니다. 웹 페이지의 메모리 누출은 종종 JavaScript 객체와 DOM 요소 간의 상호 작용을 수반합니다.
다음 장난감 예 는 프로그래머가 이벤트 리스너의 등록을 잊어 버렸을 때 발생하는 메모리 누수를 보여줍니다. 이벤트 리스너가 참조하는 객체는 가비지 수집 될 수 없습니다. 특히, iframe 창은 이벤트 리스너와 함께 누출됩니다.
// Main window:
const iframe = document.createElement('iframe');
iframe.src = 'iframe.html';
document.body.appendChild(iframe);
iframe.addEventListener('load', function() {
const local_variable = iframe.contentWindow;
function leakingListener() {
// Do something with `local_variable`.
if (local_variable) {}
}
document.body.addEventListener('my-debug-event', leakingListener);
document.body.removeChild(iframe);
// BUG: forgot to unregister `leakingListener`.
});
누출 iframe 창은 또한 모든 JavaScript 객체를 유지합니다.
// iframe.html:
class Leak {};
window.global_variable = new Leak();
메모리 누수의 근본 원인을 찾기 위해 경로를 유지한다는 개념을 이해하는 것이 중요합니다. 보관 경로는 누출 된 개체의 가비지 수집을 방지하는 개체 체인입니다. 체인은 기본 창의 전역 개체와 같은 루트 개체에서 시작됩니다. 체인이 새는 물체에서 끝납니다. 체인의 각 중간 객체는 체인의 다음 객체를 직접 참조합니다. 예를 들어 Leak
iframe 에있는 객체의 보관 경로는 다음과 같습니다.
![]() |
그림 1 : iframe 및 이벤트 리스너를 통해 유출 된 객체의 경로 유지. |
유지 경로는 JavaScript / DOM 경계 (녹색 / 빨간색으로 각각 강조 표시됨)를 두 번 교차합니다. 자바 스크립트 개체는 V8 힙에 저장되지만 DOM 개체는 Chrome의 C ++ 개체입니다.
DevTools 힙 스냅 샷
DevTools에서 힙 스냅 샷을 찍어 개체의 유지 경로를 검사 할 수 있습니다. 힙 스냅 샷은 V8 힙의 모든 오브젝트를 정확하게 캡처합니다. 최근까지는 C ++ DOM 객체에 대한 대략적인 정보 만있었습니다. 예를 들어 Chrome 65에서는 Leak
장난감 예의 개체 가 불완전하게 유지되는 경로를 보여줍니다 .
![]() |
그림 2 : Chrome에서 경로 유지 65. |
첫 번째 행만 정확합니다. Leak
객체는 실제로 global_variable
iframe의 window 객체에 저장됩니다 . 후속 행은 실제 유지 경로를 근사하고 메모리 누수의 디버깅을 어렵게 만듭니다.
Chrome 66부터 DevTools는 C ++ DOM 개체를 추적하고 개체 간의 참조를 정확하게 캡처합니다. 이는 이전에 교차 컴포넌트 가비지 콜렉션에 도입 된 강력한 C ++ 오브젝트 추적 메커니즘을 기반으로합니다. 결과적으로 DevTools의 유지 경로 가 실제로 올바른 것입니다.
![]() |
그림 3 : Chrome에서 경로 유지 66 |
후드 : 교차 구성 요소 추적
DOM 개체는 화면의 실제 텍스트와 이미지로 DOM을 변환하는 역할을하는 Chrome의 렌더링 엔진 인 Blink에서 관리합니다. Blink와 DOM 표현은 C ++로 작성되었으므로 DOM을 JavaScript에 직접 노출 할 수 없습니다. 대신 DOM의 객체는 두 가지로 나뉩니다. 하나는 JavaScript에서 사용할 수있는 V8 래퍼 객체이고 다른 하나는 DOM에서 노드를 나타내는 C ++ 객체입니다. 이러한 객체는 서로 직접 참조됩니다. Blink 및 V8과 같은 여러 구성 요소에서 개체의 생명 및 소유권을 결정하는 것은 어려운 일입니다. 모든 관련 당사자가 어떤 개체가 아직 살아 있고 어떤 개체가 재생 될 수 있는지에 대해 동의해야하기 때문입니다.
Chrome 56 이하 버전 (즉, 2017 년 3 월까지)에서 Chrome은 객체 그룹화 라는 메커니즘을 사용했습니다.생기를 결정하기. 개체는 문서의 봉쇄를 기반으로 그룹에 할당되었습니다. 모든 객체를 포함하는 그룹은 단일 객체가 다른 유지 경로를 통해 활성 상태로 유지되는 한 활성 상태를 유지했습니다. 이것은 소위 DOM 트리를 형성하면서 DOM 노드가 포함 된 문서를 항상 참조하는 DOM 노드의 맥락에서 의미가 있습니다. 그러나이 추상화는 실제 유지 경로를 모두 제거 했으므로 그림 2와 같이 디버깅에 사용하기가 어려웠습니다.이 시나리오에 맞지 않는 객체의 경우 (예 : JavaScript가 이벤트 리스너로 사용됨)이 방법도 번거로워졌습니다 JavaScript 래퍼 객체가 조기에 수집되어 다양한 속성을 잃을 빈 JS 래퍼로 바뀌는 다양한 버그가 발생했습니다.
Chrome 57부터는이 접근 방식이 크로스 컴포넌트 추적 (cross-component tracing)으로 대체되었습니다.이 추적은 JavaScript에서 DOM의 C ++ 구현을 추적하여 생동감을 결정하는 메커니즘입니다. 우리는 C ++ 측에서 점진적인 추적을 구현하여 이전 장의 블로그 게시물 에서 이야기했던 모든 세계를 멈추지 않게했습니다 . 교차 구성 요소 추적은 더 나은 대기 시간을 제공 할뿐만 아니라 구성 요소 경계를 넘나 드는 개체의 활성도를 대략적으로 추정하고 누출을 유발하는 여러 시나리오 를 수정 합니다. 또한 DevTools는 그림 3과 같이 DOM을 실제로 나타내는 스냅 샷을 제공 할 수 있습니다.