이 섹션에서는 메모리 분석에 사용되는 일반적인 용어를 설명하며 여러 언어의 다양한 메모리 프로파일링 도구에 적용할 수 있습니다.
여기에 설명된 용어와 개념은 Chrome DevTools 힙 프로파일러를 참조하세요. Java, .NET 또는 다른 메모리 프로파일러로 작업해 본 적이 있다면 이 정보가 복습이 될 수 있습니다.
객체 크기
메모리를 기본 유형 (예: 숫자 및 문자열)과 객체 (연관 배열)가 있는 그래프로 생각하세요. 다음과 같이 상호 연결된 여러 점의 그래프로 시각적으로 표현할 수 있습니다.
객체는 두 가지 방법으로 메모리를 보유할 수 있습니다.
- 객체 자체에서 직접적으로
- 암시적으로 다른 객체에 대한 참조를 보유하여 이러한 객체가 가비지 컬렉터 (줄여서 GC)에 의해 자동으로 폐기되지 않도록 합니다.
DevTools에서 힙 프로파일러('프로필'에 있는 메모리 문제를 조사하는 도구)를 사용하면 몇 가지 다른 열의 정보를 보게 될 가능성이 높습니다. 그중 가장 눈에 띄는 두 가지는 Shallow Size와 Retained Size이지만, 이것이 의미하는 바는 무엇일까요?
Shallow Size
객체 자체가 보유한 메모리 크기입니다.
일반적인 JavaScript 객체에는 설명 및 즉시 값을 저장하기 위한 약간의 메모리가 있습니다. 일반적으로 배열과 문자열만 상당히 얕은 크기를 가질 수 있습니다. 그러나 문자열과 외부 배열은 렌더러 메모리에 기본 저장소를 두는 경우가 많으므로 JavaScript 힙에 작은 래퍼 객체만 노출됩니다.
렌더기 메모리는 검사된 페이지가 렌더링되는 프로세스의 모든 메모리로, 네이티브 메모리 + 페이지의 JS 힙 메모리 + 페이지에서 시작한 모든 전용 작업자의 JS 힙 메모리입니다. 그럼에도 불구하고 작은 객체라도 많은 양의 메모리를 보유할 수 있는데, 이는 자동 가비지 컬렉션 프로세스에서 다른 객체가 폐기되지 않도록 하기 때문입니다.
유지된 크기
객체 자체가 GC 루트에서 도달할 수 없게 된 종속 객체와 함께 삭제되면 확보되는 메모리 크기입니다.
GC 루트는 네이티브 코드에서 V8 외부의 자바스크립트 객체를 참조할 때 생성되는 핸들로 구성됩니다 (로컬 또는 전역). 이러한 모든 핸들은 GC 루트 > 핸들 범위 및 GC 루트 > 전역 핸들 아래의 힙 스냅샷 내에서 찾을 수 있습니다. 브라우저 구현을 자세히 다루지 않고 이 문서에서 핸들을 설명하면 혼란스러울 수 있습니다. GC 루트와 핸들은 둘 다 개발자가 신경 쓰지 않아도 되는 부분입니다.
많은 내부 GC가 있고 대부분은 사용자가 별 관심을 기울이지 않습니다. 애플리케이션 관점에서 보면 다음과 같은 종류의 루트가 있습니다.
- Window 전역 객체 (각 iframe에 있음) 힙 스냅샷에는 창에서 가장 짧은 유지 경로에 있는 속성 참조의 거리 필드가 있습니다.
- 문서를 순회하여 도달할 수 있는 모든 네이티브 DOM 노드로 구성된 문서 DOM 트리입니다. 일부는 JS 래퍼가 없을 수도 있지만 래퍼가 있으면 문서가 활성화된 동안 래퍼가 활성화됩니다.
- 경우에 따라 디버거 컨텍스트 및 DevTools 콘솔에 의해 객체가 유지될 수 있습니다 (예: 콘솔 평가 후). 콘솔을 비우고 디버거에 활성 중단점이 없는 상태에서 힙 스냅샷을 생성합니다.
메모리 그래프는 루트로 시작되며 브라우저의 window
객체 또는 Node.js 모듈의 Global
객체일 수 있습니다. 이 루트 객체가 GC되는 방식은 제어할 수 없습니다.
루트에서 연결할 수 없는 것은 무엇이든 GC가 됩니다.
객체 보존 트리
힙은 상호 연결된 객체의 네트워크입니다. 수학에서는 이 구조를 그래프 또는 메모리 그래프라고 합니다. 그래프는 에지로 연결된 노드로 구성되며, 두 노드 모두에 라벨이 지정됩니다.
- 노드 (또는 객체)에는 노드를 빌드하는 데 사용된 생성자 함수의 이름을 사용하여 라벨이 지정됩니다.
- 에지는 속성의 이름을 사용하여 라벨이 지정됩니다.
힙 프로파일러를 사용하여 프로필을 기록하는 방법을 알아보세요. 아래의 힙 프로파일러 기록에서 눈길을 끄는 요소로는 거리, 즉 GC 루트로부터의 거리가 있습니다. 동일한 유형의 거의 모든 객체가 같은 거리에 있고 몇 개가 더 멀리 있다면 조사해 볼 가치가 있습니다.
도미네이터
도미네이터 객체는 각 객체에 정확히 하나의 도미네이터가 있기 때문에 트리 구조로 구성됩니다. 객체의 도미네이터에는 자신이 지배하는 객체에 관한 직접 참조가 없을 수 있습니다. 즉, 도미네이터의 트리는 그래프의 스패닝 트리가 아닙니다.
아래 다이어그램에서:
- 노드 1은 노드 2를 지배합니다.
- 노드 2는 노드 3, 4, 6을 지배합니다.
- 노드 3은 노드 5를 지배합니다.
- 노드 5는 노드 8을 지배합니다.
- 노드 6은 노드 7을 지배합니다.
아래 예에서 노드 #3
는 #10
의 도미네이터이지만, #7
도 GC에서 #10
로의 모든 단순 경로에 존재합니다. 따라서 루트에서 객체 A로 이어지는 모든 단순 경로에 객체 B가 존재하면 객체 B는 객체 A의 도미네이터가 됩니다.
V8 사양
메모리를 프로파일링할 때 힙 스냅샷이 특정한 방식으로 보이는 이유를 이해하면 도움이 됩니다. 이 섹션에서는 특히 V8 JavaScript 가상 머신 (V8 VM 또는 VM)에 해당하는 메모리 관련 주제를 설명합니다.
JavaScript 객체 표현
세 가지 기본 유형이 있습니다.
- 숫자 (예: 3.14159..)
- 불리언 (true 또는 false)
- 문자열 (예: 'Werner Heisenberg')
이들은 다른 값을 참조할 수 없으며 항상 리프 또는 말단 노드입니다.
숫자는 다음 중 하나로 저장할 수 있습니다.
- 작은 정수 (SMI)라고 하는 즉치 31비트 정수 값, 또는
- 힙 숫자라고 하는 힙 객체 힙 숫자는 double과 같이 SMI 형식에 맞지 않는 값을 저장하거나 값을 박싱해야 하는 경우(예: 속성 설정)에 사용됩니다.
문자열은 다음 중 하나에 저장할 수 있습니다.
- VM 힙 또는
- 외부 렌더러의 메모리에 저장됩니다. 래퍼 객체를 생성하여 외부 저장소에 액세스하는 데 사용합니다. 예를 들어 웹에서 수신한 스크립트 소스와 기타 콘텐츠는 VM 힙으로 복사되지 않고 여기에 저장됩니다.
새 자바스크립트 객체의 메모리는 전용 자바스크립트 힙 (또는 VM 힙)에서 할당됩니다. 이러한 객체는 V8의 가비지 컬렉터에서 관리하므로 이에 대한 강력한 참조가 하나 이상 있는 한 활성 상태로 유지됩니다.
네이티브 객체는 자바스크립트 힙에 없는 나머지 모든 객체입니다. 힙 객체와 달리 네이티브 객체는 전체 기간 동안 V8 가비지 컬렉터에 의해 관리되지 않으며 JavaScript 래퍼 객체를 사용하여 JavaScript에서만 액세스할 수 있습니다.
Cons 문자열은 저장 후 조인되는 문자열 쌍으로 구성된 객체로, 연결의 결과입니다. cons string 콘텐츠의 조인은 필요한 경우에만 발생합니다. 예를 들어 조인된 문자열의 하위 문자열을 생성해야 하는 경우가 있습니다.
예를 들어 a와 b를 연결하면 연결 결과를 나타내는 문자열 (a, b)를 얻게 됩니다. 나중에 d를 이 결과에 연결하면 또 다른 cons 문자열 ((a, b), d)을 얻게 됩니다.
배열 - 배열은 숫자 키가 포함된 객체입니다. V8 VM에서 대량의 데이터를 저장하기 위해 광범위하게 사용됩니다 사전처럼 사용되는 키-값 쌍 집합은 배열로 백업됩니다.
일반적인 JavaScript 객체는 저장에 사용되는 두 가지 배열 유형 중 하나일 수 있습니다.
- 이름이 지정된 속성
- 숫자 요소
속성의 수가 극히 적은 경우 자바스크립트 객체 자체에 내부적으로 저장할 수 있습니다.
매핑 - 객체의 종류와 레이아웃을 설명하는 객체입니다. 예를 들어 맵은 빠른 속성 액세스를 위해 암시적 객체 계층 구조를 설명하는 데 사용됩니다.
객체 그룹
각 네이티브 객체 그룹은 서로에 대한 상호 참조를 보유한 객체로 구성됩니다. 모든 노드가 상위 요소에 대한 링크와 다음 하위 요소 및 다음 동위 항목에 연결되어 연결된 그래프를 형성하는 DOM 하위 트리를 예로 들어보겠습니다. 네이티브 객체는 자바스크립트 힙에 표시되지 않기 때문에 크기가 0입니다. 대신 래퍼 객체가 생성됩니다.
각 래퍼 객체는 명령어를 리디렉션하기 위해 상응하는 네이티브 객체 참조를 보유합니다. 객체 그룹은 그 자체로 래퍼 객체를 보유합니다. 하지만 GC는 래퍼가 더 이상 참조되지 않는 객체 그룹을 해제할 만큼 스마트하기 때문에 수집 불가능한 주기가 생성되지는 않습니다. 하지만 단일 래퍼를 해제하는 것을 잊으면 전체 그룹 및 관련 래퍼가 유지됩니다.