힙 스냅샷 기록

Meggin Kearney
Meggin Kearney
Sofia Emelianova
Sofia Emelianova

메모리 > 프로필 > 힙 스냅샷에서 힙 스냅샷을 기록하고 메모리 누수를 찾는 방법을 알아보세요.

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

스냅샷 만들기

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

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

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

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

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

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

분산된 Item 객체의 힙 스냅샷입니다.

스냅샷 지우기

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

모든 프로필을 삭제합니다.

스냅샷 보기

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

View 콘텐츠 목적
요약 생성자 이름으로 그룹화된 객체 유형에 따라 객체와 메모리 사용량을 추적하는 데 사용하세요. DOM 누수 추적에 유용합니다.
비교 두 스냅샷의 차이점 작업 전과 후의 스냅샷을 2개 이상 비교하는 데 사용합니다. 해제된 메모리의 델타와 참조 횟수를 검사하여 메모리 누수의 존재 여부와 원인을 확인합니다.
격리 힙 콘텐츠 객체 구조를 더 잘 보여주고, 전역 네임스페이스 (창)에서 참조된 객체를 분석하여 객체를 유지하는 데 도움을 줍니다. 이를 통해 클로저를 분석하고 객체를 하위 수준에서 자세히 살펴볼 수 있습니다.
통계 메모리 할당 원형 차트 코드, 문자열, JS 배열, 유형이 지정된 배열 및 시스템 객체에 할당된 메모리 부분의 실제 크기를 확인합니다.

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

요약 뷰

처음에는 힙 스냅샷이 열에 생성자를 나열하는 요약 뷰에서 열립니다. 생성자를 확장하여 생성자가 인스턴스화한 객체를 확인할 수 있습니다.

확장된 생성자가 있는 요약 뷰

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

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

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

생성자를 펼치면 요약 뷰에 생성자의 모든 인스턴스가 표시됩니다. 각 인스턴스는 해당 열에서 얕은 크기 및 유지 크기에 대한 분석을 가져옵니다. @ 문자 다음에 오는 숫자가 객체의 고유 ID입니다. 이를 통해 객체별로 힙 스냅샷을 비교할 수 있습니다.

생성자 필터

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

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

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

요약의 특별 항목

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

  • 내장 함수(예: Array 또는 Object)
  • 코드에 정의한 함수
  • 생성자를 기반으로 하지 않는 특수 카테고리입니다.

생성자 항목

(array)

이 카테고리에는 JavaScript에 표시되는 객체에 직접 대응하지 않는 내부 배열과 같은 다양한 객체가 포함됩니다.

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

(compiled code)

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

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

(concatenated string)

V8이 두 문자열을 연결할 때(예: JavaScript + 연산자 사용 시) 결과를 내부적으로 '연결된 문자열'(Rope 데이터 구조라고도 함)으로 표현하도록 선택할 수 있습니다.

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

InternalNode

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

C++ 클래스 이름을 보려면 Chrome for Testing을 사용하여 다음 작업을 실행하세요.

  1. DevTools를 열고 설정 설정 > 실험 > check_box 힙 스냅샷에 내부 노출 옵션 표시를 사용 설정합니다.
  2. 메모리 패널을 열고 radio_button_checked 힙 스냅샷을 선택한 다음 radio_button_checked 내부 정보 노출 (구현별 추가 세부정보 포함)을 사용 설정합니다.
  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. 두 번째 힙 스냅샷을 만들고 뷰를 Comparison으로 변경하고 스냅샷 1과 비교합니다.

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

스냅샷 1과 비교합니다.

격리 뷰

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

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

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

Containment 뷰입니다.

자문 서비스 섹션

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

자문 서비스 섹션.

이 예시에서는 선택된 문자열이 Item 인스턴스의 x 속성으로 유지됩니다.

보관자 무시

보관기를 숨겨서 선택한 개체를 보관하는 다른 객체를 찾을 수 있습니다. 이 옵션을 사용하면 코드에서 이 보관자를 먼저 삭제한 다음 힙 스냅샷을 다시 만들 필요가 없습니다.

드롭다운 메뉴의 '이 보관자 무시' 옵션.

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

특정 객체 찾기

수집된 힙에서 객체를 찾으려면 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 규칙)와 자바스크립트 객체 간의 양방향 종속 항목을 반영하는 기능이 있습니다. 이렇게 하면 주위에 떠다니는 분리된 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 하위 트리