힙 스냅샷 기록

Meggin Kearney
Meggin Kearney
Sofia Emelianova
Sofia Emelianova

메모리 > 프로필 > 힙 스냅샷으로 힙 스냅샷을 기록하고 메모리 누수를 찾는 방법을 알아봅니다.

힙 프로파일러는 페이지의 JavaScript 객체 및 관련 DOM 노드별로 메모리 분포를 보여줍니다. 이를 사용하여 JS 힙 스냅샷을 촬영하고, 메모리 그래프를 분석하고, 스냅샷을 비교하고, 메모리 누수를 찾을 수 있습니다. 자세한 내용은 트리를 보존하는 객체를 참고하세요.

스냅샷 촬영

힙 스냅샷을 찍으려면 다음 단계를 따르세요.

  1. 프로파일링할 페이지에서 DevTools를 열고 메모리 패널로 이동합니다.
  2. 힙 스냅샷 프로파일링 유형을 선택한 다음 JavaScript VM 인스턴스를 선택하고 스냅샷 찍기를 클릭합니다.

선택한 프로파일링 유형 및 JavaScript VM 인스턴스

메모리 패널이 스냅샷을 로드하고 파싱하면 힙 스냅샷 섹션의 스냅샷 제목 아래에 연결 가능한 JavaScript 객체의 총 크기가 표시됩니다.

연결 가능한 객체의 총 크기입니다.

스냅샷에는 전역 객체에서 연결할 수 있는 메모리 그래프의 객체만 표시됩니다. 스냅샷 촬영은 항상 가비지 수집부터 시작합니다.

흩어진 Item 객체의 힙 스냅샷입니다.

스냅샷 지우기

모든 스냅샷을 삭제하려면 모든 프로필 지우기를 클릭합니다.

모든 프로필을 지웁니다.

스냅샷 보기

다양한 목적으로 다양한 관점에서 스냅샷을 검사하려면 상단의 드롭다운 메뉴에서 다음 보기 중 하나를 선택합니다.

보기 콘텐츠 목적
요약 생성자 이름을 기준으로 그룹화된 객체 이 옵션을 사용하면 유형을 기준으로 객체와 메모리 사용량을 추적할 수 있습니다. DOM 누수 추적에 유용합니다.
비교 두 스냅샷의 차이점 이 옵션을 사용하면 두 개 (또는 그 이상의) 스냅샷을 작업 전과 후에 비교하여 볼 수 있습니다. 비워진 메모리의 델타와 참조 카운트를 검사하여 메모리 누수의 존재 여부와 원인을 확인합니다.
격리 힙 콘텐츠 객체 구조를 좀 더 잘 볼 수 있게 해주므로 전역 네임스페이스 (창)에서 참조된 객체를 분석하여 이를 유지하는 것이 무엇인지 알아낼 수 있습니다. 이 옵션을 사용하면 클로저를 분석하고 객체를 낮은 단계부터 심층적으로 접근할 수 있습니다.
통계 메모리 할당 원형 차트 코드, 문자열, JS 배열, 유형 배열, 시스템 객체에 할당된 메모리 부분의 상대 크기를 확인합니다.

상단의 드롭다운 메뉴에서 선택한 요약 보기

요약 보기

처음에는 요약 뷰에 생성자가 열에 나열된 힙 스냅샷이 열립니다. 생성자를 펼쳐 인스턴스화된 객체를 볼 수 있습니다.

생성자가 펼쳐진 요약 보기

관련 없는 생성자를 필터링하려면 요약 뷰 상단의 클래스 필터에 검사하려는 이름을 입력합니다.

생성자 이름 옆의 숫자는 생성자로 생성된 총 객체 수를 나타냅니다. 요약 보기에는 다음 열도 표시됩니다.

  • 거리는 최단 거리의 단순한 노드 경로를 사용하여 루트까지의 거리를 표시합니다.
  • Shallow Size는 특정 생성자로 생성된 모든 객체의 Shallow Size 총합을 보여줍니다. Shallow Size란 객체 자체가 보유한 메모리 크기를 말합니다. 일반적으로 배열과 문자열은 더 큰 Shallow Size를 가집니다. 객체 크기도 참고하세요.
  • Retained size는 동일한 객체 집합 중에서 최대 보존 크기를 보여줍니다. 보존 크기는 객체를 삭제하고 종속 항목에 더 이상 연결할 수 없게 하여 확보할 수 있는 메모리 크기입니다. 객체 크기도 참고하세요.

생성자를 확장하면 요약 뷰에 모든 인스턴스가 표시됩니다. 각 인스턴스는 해당 열에서 Shallow Size 및 보존 크기의 세부정보를 가져옵니다. @ 문자 뒤의 숫자는 객체의 고유 ID입니다. 이를 통해 힙 스냅샷을 객체별로 비교할 수 있습니다.

생성자 필터

요약 보기를 사용하면 비효율적인 메모리 사용의 일반적인 사례를 기반으로 생성자를 필터링할 수 있습니다.

이러한 필터를 사용하려면 작업 표시줄에서 가장 오른쪽에 있는 드롭다운 메뉴에서 다음 옵션 중 하나를 선택합니다.

  • 모든 객체: 현재 스냅샷에서 캡처한 모든 객체입니다. 기본적으로 설정됩니다.
  • 스냅샷 1 전에 할당된 객체: 첫 번째 스냅샷을 찍기 전에 생성되어 메모리에 남아 있는 객체입니다.
  • 스냅샷 1과 스냅샷 2 사이에 할당된 객체: 최신 스냅샷과 이전 스냅샷 간의 객체 차이를 확인합니다. 새 스냅샷을 추가할 때마다 드롭다운 목록에 이 필터의 증분 값이 추가됩니다.
  • 중복 문자열: 메모리에 여러 번 저장된 문자열 값입니다.
  • 분리된 노드에 의해 유지되는 객체: 분리된 DOM 노드가 참조하기 때문에 활성 상태로 유지되는 객체입니다.
  • DevTools 콘솔에 보관된 객체: DevTools 콘솔을 통해 평가되거나 상호작용했기 때문에 메모리에 보관된 객체입니다.

요약의 특수 항목

요약 뷰는 생성자를 기준으로 그룹화하는 것 외에도 다음을 기준으로 객체를 그룹화합니다.

  • Array 또는 Object와 같은 내장 함수
  • 태그(예: <div>, <a>, <img> 등)별로 그룹화된 HTML 요소
  • 코드에서 정의한 함수입니다.
  • 생성자를 기반으로 하지 않는 특수 카테고리입니다.

생성자 항목

(array)

이 카테고리에는 JavaScript에 표시되는 객체와 직접 일치하지 않는 다양한 내부 배열과 유사한 객체가 포함됩니다.

예를 들어 JavaScript Array 객체의 콘텐츠는 크기를 더 쉽게 조절할 수 있도록 (object elements)[]라는 보조 내부 객체에 저장됩니다. 마찬가지로 JavaScript 객체의 이름이 지정된 속성은 (array) 카테고리에 표시되는 (object properties)[]라는 보조 내부 객체에 저장되는 경우가 많습니다.

(compiled code)

이 카테고리에는 JavaScript 또는 WebAssembly로 정의된 함수를 실행할 수 있도록 V8에 필요한 내부 데이터가 포함됩니다. 각 함수는 작고 느린 함수에서 크고 빠른 함수에 이르기까지 다양한 방식으로 표현할 수 있습니다.

V8은 이 카테고리의 메모리 사용량을 자동으로 관리합니다. 함수가 여러 번 실행되면 V8은 더 빠르게 실행할 수 있도록 해당 함수에 더 많은 메모리를 사용합니다. 함수가 한동안 실행되지 않으면 V8에서 해당 함수의 내부 데이터를 삭제할 수 있습니다.

(concatenated string)

V8이 JavaScript + 연산자와 같이 두 문자열을 연결하면 결과를 내부적으로 로프 데이터 구조라고도 하는 '연결된 문자열'로 나타낼 수 있습니다.

V8은 두 소스 문자열의 모든 문자를 새 문자열로 복사하는 대신 두 소스 문자열을 가리키는 firstsecond라는 내부 필드가 있는 작은 객체를 할당합니다. 이렇게 하면 V8에서 시간과 메모리를 절약할 수 있습니다. JavaScript 코드의 관점에서 보면 이는 일반 문자열일 뿐이며 다른 문자열과 동일하게 작동합니다.

InternalNode

이 카테고리는 Blink에서 정의한 C++ 객체와 같이 V8 외부에 할당된 객체를 나타냅니다.

C++ 클래스 이름을 보려면 테스트용 Chrome을 사용하고 다음 단계를 따르세요.

  1. DevTools를 열고 Settings > Experiments > Show option to expose internals in heap snapshots를 사용 설정합니다.
  2. 메모리 패널을 열고 힙 스냅샷을 선택한 다음 내부 데이터 표시 (구현 관련 추가 세부정보 포함)를 사용 설정합니다.
  3. InternalNode가 많은 메모리를 유지하도록 하는 문제를 재현합니다.
  4. 힙 스냅샷을 찍습니다. 이 스냅샷에서 객체에는 InternalNode 대신 C++ 클래스 이름이 있습니다.
(object shape)

V8의 빠른 속성에 설명된 대로 V8은 숨겨진 클래스 (또는 도형)를 추적하여 동일한 속성을 동일한 순서로 갖는 여러 객체를 효율적으로 표현할 수 있습니다. 이 카테고리에는 system / Map (JavaScript Map와 관련 없음)라는 숨겨진 클래스와 관련 데이터가 포함됩니다.

(sliced string)

JavaScript 코드가 String.prototype.substring()를 호출할 때와 같이 V8이 하위 문자열을 가져와야 하는 경우 V8은 원래 문자열에서 관련 문자를 모두 복사하는 대신 슬라이스된 문자열 객체를 할당할 수 있습니다. 이 새 객체에는 원래 문자열에 대한 포인터가 포함되어 있으며, 원래 문자열에서 사용할 문자 범위를 설명합니다.

JavaScript 코드의 관점에서 보면 이는 일반 문자열일 뿐이며 다른 문자열과 동일하게 작동합니다. 슬라이스된 문자열이 많은 메모리를 보유하고 있다면 프로그램이 문제 2869를 트리거했을 수 있으며 슬라이스된 문자열을 '평면화'하기 위한 의도적인 조치를 취하는 것이 좋습니다.

system / Context

system / Context 유형의 내부 객체에는 중첩 함수가 액세스할 수 있는 JavaScript 범위인 클로저의 로컬 변수가 포함됩니다.

모든 함수 인스턴스에는 이러한 변수에 액세스할 수 있도록 함수가 실행되는 Context에 대한 내부 포인터가 포함되어 있습니다. Context 객체는 JavaScript에서 직접 볼 수 없지만 직접 제어할 수 있습니다.

(system)

이 카테고리에는 아직 더 의미 있는 방식으로 분류되지 않은 다양한 내부 객체가 포함되어 있습니다.

비교 보기

비교 뷰를 사용하면 여러 스냅샷을 서로 비교하여 누수된 객체를 찾을 수 있습니다. 예를 들어 문서를 열었다가 닫는 것처럼 작업을 실행했다가 실행을 역전시켜도 여분의 객체가 남아서는 안 됩니다.

특정 작업이 누수를 일으키지 않는지 확인하려면 다음 단계를 따르세요.

  1. 작업을 실행하기 전에 힙 스냅샷을 찍습니다.
  2. 작업을 실행합니다. 즉, 누수가 발생할 수 있다고 생각되는 방식으로 페이지와 상호작용합니다.
  3. 역방향 작업을 실행합니다. 즉, 상호작용의 반대를 수행하고 이를 몇 번 반복합니다.
  4. 두 번째 힙 스냅샷을 찍고 뷰를 비교로 변경하여 스냅샷 1과 비교합니다.

비교 뷰에는 두 스냅샷의 차이점이 표시됩니다. total 항목을 확장하면 추가되고 삭제된 객체 인스턴스가 표시됩니다.

스냅샷 1과 비교한 결과입니다.

Containment 뷰

Containment 뷰는 애플리케이션의 객체 구조를 '조감도' 형태로 보여줍니다. 이 뷰를 사용하면 함수 클로저 안쪽을 들여다보고, JavaScript 객체를 구성하는 VM 내부 객체를 관찰하고, 애플리케이션이 얼마나 많은 메모리를 사용하는지 아주 낮은 수준에서 파악할 수 있습니다.

이 뷰는 여러 진입점을 제공합니다.

  • DOMWindow 객체. JavaScript 코드의 전역 객체입니다.
  • GC 루트 VM의 가비지 컬렉터에서 사용하는 GC 루트입니다. GC 루트는 기본 제공 객체 맵, 기호 테이블, VM 스레드 스택, 컴파일 캐시, 핸들 범위, 전역 핸들 등으로 구성될 수 있습니다.
  • 네이티브 객체. 자동화를 위해 JavaScript 가상 머신 내부로 '푸시'되는 브라우저 객체입니다(예: DOM 노드, CSS 규칙).

Containment 뷰

Retainers 섹션

메모리 패널 하단의 유지자 섹션에는 뷰에서 선택한 객체를 가리키는 객체가 표시됩니다. 통계를 제외한 뷰에서 다른 객체를 선택하면 메모리 패널의 유지자 섹션이 업데이트됩니다.

&#39;유지보수&#39; 섹션

이 예에서 선택한 문자열은 Item 인스턴스의 x 속성에 의해 유지됩니다.

보관자 무시

리테이너를 숨겨 다른 객체가 선택한 객체를 유지하는지 확인할 수 있습니다. 이 옵션을 사용하면 먼저 코드에서 이 리테이너를 삭제한 다음 힙 스냅샷을 다시 찍을 필요가 없습니다.

드롭다운 메뉴의 &#39;이 유지보수 계약 무시&#39; 옵션

리테이너를 숨기려면 마우스 오른쪽 버튼을 클릭하고 이 리테이너 무시를 선택합니다. 무시된 리테이너는 거리 열에 ignored로 표시됩니다. 모든 리테이너 무시를 중지하려면 상단의 작업 표시줄에서 무시된 리테이너 복원을 클릭합니다.

특정 객체 찾기

수집된 힙에서 객체를 찾으려면 Ctrl + F를 사용하고 객체 ID를 입력하여 검색하면 됩니다.

함수에 이름을 지정하여 각 클로저 구별

함수의 이름을 지정하면 스냅샷에서 클로저를 구분할 수 있으므로 많은 도움이 됩니다.

예를 들어 다음 코드는 이름이 지정된 함수를 사용하지 않습니다.

function createLargeClosure() {
  var largeStr = new Array(1000000).join('x');

  var lC = function() { // this is NOT a named function
    return largeStr;
  };

  return lC;
}

하지만 다음 예시에서는 사용합니다.

function createLargeClosure() {
  var largeStr = new Array(1000000).join('x');

  var lC = function lC() { // this IS a named function
    return largeStr;
  };

  return lC;
}

폐쇄에서 이름이 지정된 함수

DOM 누수 찾기

힙 프로파일러는 브라우저 네이티브 객체 (DOM 노드 및 CSS 규칙)와 JavaScript 객체 간의 양방향 종속성을 반영하는 기능이 있습니다. 이를 통해 분리된 DOM 하위 트리가 잊힌 채로 부유하는 현상으로 인해 다른 방식으로는 보이지 않는 누수 발생을 발견하는 데 도움이 됩니다.

DOM 누수는 생각보다 규모가 클 수 있습니다. 다음 예를 참고하세요. #tree 가비지는 언제 수집되나요?

  var select = document.querySelector;
  var treeRef = select("#tree");
  var leafRef = select("#leaf");
  var body = select("body");

  body.removeChild(treeRef);

  //#tree can't be GC yet due to treeRef
  treeRef = null;

  //#tree can't be GC yet due to indirect
  //reference from leafRef

  leafRef = null;
  //#NOW #tree can be garbage collected

#leaf는 자신의 부모 (parentNode)에 대한 참조를 유지하며 #tree까지 재귀적으로 순환하므로, leafRef가 무효화되었을 때에만 #tree 아래의 전체 트리가 GC 후보가 됩니다.

DOM 하위 트리