본문으로 건너뛰기

JS 메모리 누수 조사 방법

무료2017-08-06#JS#JS内存泄漏#闭包内存泄漏#JS内存问题#javascript memory leak#JS内存性能

메모리 문제 발견 방법, 조사 방법, 해결 방법, 예방 방법

서문에

JS 의 메모리 문제는 단일 페이지 애플리케이션 (SPA) 에서 자주 발생합니다. 일반적으로 다음과 같은 상황 특징이 있습니다:

  • 페이지 수명 주기가 김 (사용자가 10 분, 30 분, 심지어 2 시간까지 잔존할 수 있음)

  • 상호작용 기능이 많음 (페이지가 표시보다 기능 중심)

  • JS 중시 (프론트엔드에 복잡한 데이터 상태, 뷰 관리가 있음)

메모리 누수는 누적적인 과정이며, 페이지 수명 주기가 다소 길 때만 문제가 됩니다 (소위 "새로고침하면 부활"). 빈번한 상호작용은 이 누적 과정을 가속화하며, 표시 중심의 페이지에서는 이러한 문제가 표면화되기 어렵습니다. 마지막으로, JS 로직이 상대적으로 복잡하지 않으면 메모리 문제는 발생하지 않습니다 ("버그가 많은 이유는 코드 양이 많아서 나 자신도 파악하지 못한다"). 단순한 폼 검증 제출 정도에서는 메모리에 영향을 미칠 기회가 거의 없습니다.

그렇다면 상호작용 기능이 많고 JS 로직이 복잡한 기준은 무엇일까요? 어느 정도까지가 위험할까요?

실제로는, 약간의 상호작용 기능 (예: 부분 업데이트) 이 있는 단순한 페이지라도 조금만 주의하지 않으면 메모리隐患이 남습니다. 그것이 표면화되면 메모리 문제라고 부릅니다.

一.툴 환경

툴:

  • Chrome Task Manager 툴

  • Chrome DevTools Performance 패널

  • Chrome DevTools Memory 패널

환경:

  • 안정성, 네트워크 등 변동 요인 제거 (가짜 데이터 사용)

  • 작업 재현성, "누적" 난이도 낮추기 (작업 절차 단순화, SMS 인증 등环节은 삭제 고려)

  • 간섭 없음, 플러그인 영향 배제 (시크릿 모드로 열기)

즉 (Mac 환경에서):

  1. Command + Shift + N으로 시크릿 모드 진입

  2. Command + Alt + I로 DevTools 열기

  3. URL 입력하여 페이지 열기

이제 시작할 수 있습니다

二.용어 개념

먼저 기본적인 메모리 지식을 갖추고, DevTools 가 제공하는 각 항목 기록의 의미를 이해해야 합니다

Mark-and-sweep

JS 관련 GC 알고리즘은 주로 참조 카운트 (IE 의 BOM, DOM 객체) 와 마크 앤 스위프 (주류 방식) 이며, 각각 장단점이 있습니다:

  • 참조 카운트는 회수가 적시 (참조 수가 0 이 되면 즉시 해제) 하지만, 순환 참조는 영원히 해제할 수 없음

  • 마크 앤 스위프는 순환 참조 문제가 없음 (접근 불가능하면 회수) 하지만, 회수가 적시하지 않고 Stop-The-World 필요

마크 앤 스위프 알고리즘 단계는 다음과 같습니다:

  1. GC 는 root 목록을 유지하며, root 는 일반적으로 코드 내에서 참조를 보유하는 전역 변수입니다. JS 에서 window 객체가 root 가 되는 전역 변수의 한 예입니다. window 객체는 항상 존재하므로, GC 는 그것과 그 모든 자식이 항상 존재 (비쓰레기) 한다고 간주합니다

  2. 모든 root 가 검사되고 활성 (비쓰레기) 으로 마크되며, 그 모든 자식도 재귀적으로 검사됩니다. root 에서 액세스 가능한 모든 것은 쓰레기로 취급되지 않습니다

  3. 활성으로 마크되지 않은 모든 메모리 블록은 쓰레기로 취급되며, GC 는 이를 해제하여 OS 에 반환할 수 있습니다

현대 GC 기술은 이 알고리즘에 다양한 개선을 가했지만, 본질은 모두 동일합니다: 액세스 가능한 메모리 블록이 이렇게 마크되고, 나머지가 쓰레기가 됩니다

Shallow Size & Retained Size

메모리는 기본형 (숫자 및 문자열 등) 과 객체 (연관 배열) 로 구성된 그래프로 볼 수 있습니다. 형상적으로 말하면, 메모리는 여러 상호 연결된 점으로 구성된 그래프로 표현할 수 있습니다. 다음과 같습니다:

  3-->5->7
  ^      ^
 /|      |
1 |      6-->8
 \|     /^
  v    /
  2-->4

객체는 두 가지 방법으로 메모리를 점유할 수 있습니다:

  • 객체 자신을 통해 직접 점유

  • 다른 객체에 대한 참조를 보유함으로써 간접적으로 점유하며, 이러한 객체들이 가비지 컬렉터 (이하 GC) 에 의해 자동으로 처리되는 것을 방지합니다

DevTools 의 힙 메모리 스냅샷 분석 패널에서 Shallow SizeRetained Size 는 각각 객체가 이 두 가지 방법으로 점유하는 메모리 크기를 나타냅니다

Shallow Size

객체 자신이 점유하는 메모리의 크기입니다. 일반적으로 배열과 문자열만 유의미한 Shallow Size 를 가집니다. 그러나 문자열과 외부 배열의 메인 스토리지는 일반적으로 renderer 메모리에 있으며, 작은 래퍼 객체만 JavaScript 힙 위에 둡니다

renderer 메모리는 페이지 렌더링 프로세스의 메모리 합계입니다: 네이티브 메모리 + 페이지의 JS 힙 메모리 + 페이지가 시작한 모든 전용 워커의 JS 힙 메모리. 그럼에도 불구하고 작은 객체라도 다른 객체들이 자동 가비지 컬렉션 프로세스에 의해 처리되는 것을 방지함으로써 간접적으로 대량의 메모리를 점유할 수 있습니다

Retained Size

객체 자신과それに 의존하는 객체 (GC root 에서 액세스할 수 없게 된 객체) 가 삭제된 후 해제되는 메모리 크기입니다

많은 내부 GC root 가 있으며, 그 대부분은 주목할 필요가 없습니다. 애플리케이션 관점에서 보면, GC root 에는 다음과 같은 종류가 있습니다:

  • Window 전역 객체 (각 iframe 에 존재). 힙 스냅샷에는 distance 필드가 있으며, window 에서의 최단 보유 경로상의 속성 참조 수를 나타냅니다.

  • 문서 DOM 트리, document 를 통해 액세스할 수 있는 모든 네이티브 DOM 노드로 구성됩니다. 모든 노드가 JS 래퍼를 가지는 것은 아니지만, 래퍼가 있고 document 가 활성 상태이면 래퍼도 활성 상태가 됩니다

  • 때때로 객체가 디버거 컨텍스트와 DevTools console 에 의해 보유될 수 있습니다 (예: console 에서 평가 계산 후). 따라서 힙 스냅샷을 디버깅할 때는 console 을 클리어하고 브레이크포인트를 제거해야 합니다

메모리 그래프는 root 에서 시작하며, root 는 브라우저의 window 객체 또는 Node.js 모듈의 Global 객체이며, root 객체의 가비지 컬렉션 방식은 제어할 수 없습니다

  3-->5->7   9-->10
  ^      ^
 /|      |
1 |      6-->8
 \|     /^
  v    /
  2-->4

여기서 1 은 root(루트 노드), 7 과 8 은 기본값 (리프 노드), 9 와 10 은 GC 됩니다 (고립 노드), 나머지는 객체 (비루트 비리프 노드) 입니다

Object's retaining tree

힙은 상호 연결된 객체의 네트워크입니다. 수학적으로 이러한 구조는 "그래프" 또는 메모리 그래프라고 합니다. 그래프는 주어진 레이블로 표현되는 엣지로 연결된 노드로 구성됩니다:

  • 노드 (또는 객체) 는 생성자 (노드를 구축하는 데 사용됨) 의 이름으로 레이블이 지정됩니다

  • 엣지는 속성 이름으로 레이블이 지정됩니다

distance 는 GC root 로부���의 거리를 나타냅니다. 어떤 타입의 대부분의 객체의 distance 가 모두 같고, 소수의 객체만 거리가 현저히 크다면 주의 깊게 조사해야 합니다

Dominator

지배 객체는 모두 트리 구조를 구성합니다. 각 객체에는 하나의 (직접) 지배자만 있으므로, 객체의 지배자는 지배되는 객체에 대한 직접 참조를 가지지 않을 수 있으며, 지배자 트리는 그래프의 생성 트리가 아닙니다

객체 참조 그래프에서 객체 B 로 가는 모든 경로가 객체 A 를 통과하면, A 는 B 를 지배한다고 간주합니다. 객체 A 가 객체 B 에 가장 가까운 지배자이면, A 는 B 의 직접 지배자라고 간주합니다

아래 그림에서:

  1     1 은 2 를 지배
  |     2 는 3 4 6 을 지배
  v
  2
/   \
v   v
4   3   3 은 5 를 지배
|  /|
| / |
|/  |
v   v
6   5   5 는 8 을 지배; 6 은 7 을 지배
|   |
v   v
7   8

따라서 7 의 직접 지배자는 6 이고, 7 의 지배자는 1, 2, 6 입니다

V8 의 JS 객체 표현

primitive type

3 가지 기본형:

  • 숫자

  • 불리언

  • 문자열

이들은 다른 값을 참조할 수 없으므로 항상 리프 또는 터미널 노드입니다

숫자에는 두 가지 저장 방식이 있습니다:

  • 직접적인 31 비트 정수 값은 SMI(Small Integer) 라고 합니다

  • 힙 객체, 힙 숫자 참조로. 힙 숫자는 SMI 형식에 맞지 않는 값 (예: double 형) 을 저장하거나, 값에 속성을 설정해야 하는 경우 등 박싱이 필요할 때 사용됩니다

문자열에도 두 가지 저장 방식이 있습니다:

  • VM 힙

  • renderer 메모리 (외부), 외부 저장 공간에 액세스하기 위한 래퍼 객체를 생성합니다. 예를 들어, 스크립트 소스 코드 및 웹에서 수신한 기타 콘텐츠는 VM 힙에 복사되는 것이 아니라 외부 저장 공간에 배치됩니다

새 JS 객체의 메모리는 전용 JS 힙 (또는 VM 힙) 에서 할당되며, 이러한 객체는 V8 의 GC 에 의해 관리됩니다. 따라서 그것들에 대한 강한 참조가 존재하는 한, 그것들은 활성 상태로 유지됩니다

Native Object

네이티브 객체는 JS 힙 외부의 모든 것입니다. 힙 객체와 비교하여, 네이티브 객체의 전체 수명 주기는 V8 의 GC 에 의해 관리되지 않으며, 래퍼 객체를 통해서만 JS 에서 액세스할 수 있습니다

Cons String

연결 문자열 (concatenated string) 은 저장되고 연결된 문자열 쌍으로 구성되며, 연결 문자열의 콘텐츠를 필요할 때만 연결합니다. 예를 들어, 연결 문자열의 서브스트링을 가져올 때

예를 들어, ab 를 연결하여 문자열 (a, b) 를 얻고, 이어 d 를 이 결과와 연결하면, 또 다른 연결 문자열 ((a, b), d) 를 얻습니다

Array

배열은 숫자 key 를 가진 객체입니다. V8 VM 에서 널리 사용되며, 대량의 데이터를 저장하는 데 사용되며, 사전의 키 - 값 쌍 컬렉션도 배열 형태로 (저장) 사용됩니다

전형적인 JS 객체는 두 가지 배열 타입에 해당하며, 다음을 저장하는 데 사용됩니다:

  • 이름 있는 속성

  • 숫자 요소

속성 수가 매우 적으면, JS 객체 자체 내부에 배치할 수 있습니다

Map

객체의 종류와 그 레이아웃을 설명하는 객체입니다. 예를 들어, map 은 빠른 속성 액세스를 구현하기 위한 암시적 객체 계층 구조를 설명하는 데 사용됩니다

Object group

(객체 그룹 내의) 각 네이티브 객체는 상호 참조를 보유하는 객체로 구성됩니다. 예를 들어, DOM 서브트리의 각 노드는 그 부모, 다음 자식, 다음 형제에 대한 포인터를 가지며, 따라서 연결 그래프를 형성합니다. 네이티브 객체는 JS 힙에 표시되지 않으므로 크기는 0 입니다. 래퍼 객체가 생성됩니다

각 래퍼 객체는 해당 네이티브 객체에 대한 참조를 보유하며, 명령을 자신에게 리디렉션하는 데 사용됩니다. 이렇게 객체 그룹은 래퍼 객체를 보유합니다. 그러나 회수할 수 없는 순환은 형성되지 않습니다. GC 는 똑똑해서,谁的 래퍼가 더 이상 참조되지 않는지 감지하고 해당 객체 그룹을 해제합니다. 그러나 래퍼 해제를 잊으면, 객체 그룹 전체와 관련 래퍼를 보유하게 됩니다

三.툴 사용법

Task Manager

메모리 사용 상황을 대략적으로 확인하는 데 사용

입구는 오른쪽 상단 세 점 -> 추가 도구 -> 작업 관리자입니다. 그런 다음 오른쪽 클릭 헤더 -> JS 메모리 사용량 체크합니다. 주로 두 열에 주목합니다:

  • 메모리 열은 네이티브 메모리를 나타냅니다. DOM 노드는 네이티브 메모리에 저장됩니다. 이 값이 증가하고 있으면 DOM 노드가 생성되고 있음을 의미합니다

  • JS 메모리 사용량 열은 JS 힙을 나타냅니다. 이 열에는 두 개의 값이 포함되어 있으며, 실시간 값 (괄호 안의 숫자) 에 주목해야 합니다. 실시간 숫자는 페이지의 액세스 가능한 객체가 사용 중인 메모리 양을 나타냅니다. 이 숫자가 증가하고 있으면 새 객체가 생성되거나 기존 객체가 성장하고 있는 것입니다

Performance

메모리 변화 추이를 관찰하는 데 사용

입구는 DevTools 의 Performance 패널입니다. 그런 다음 Memory 를 체크합니다. 페이지 첫 로딩 시 메모리 사용 상황을 보고 싶으면, Command + R로 페이지를 새로고침하면 로딩 과정 전체를 자동으로 기록합니다. 특정 작업 전후의 메모리 변화를 보고 싶으면, 작업 전에 "검은 점" 버튼을 클릭하여 기록을 시작하고, 작업 완료 후 "빨간 점" 버튼을 클릭하여 기록을 종료합니다

기록 완료 후 중부의 JS Heap 을 체크합니다. 파란색 꺾은선이 메모리 변화 추이를 나타냅니다. 전체 추이가 지속적으로 상승하고 크게 하락하지 않으면, 수동 GC 로 확인합니다: 다시 한 번 작업 기록을 수행하고, 작업 종료 전 또는 중간에 몇 번 수동 GC("검은 쓰레기통" 버튼 클릭) 를 수행합니다. GC 시점에 꺾은선이 크게 하락하지 않고 전체 추이가 지속적으로 상승하면, 메모리 누수가 존재할 수 있습니다

또는 더 과격한 확인 방법: 기록 시작 -> 작업 50 회 반복 -> 자동 GC 에 의한 큰 하락 있는지 확인. 사용 메모리 크기가 임계값에 도달하면 자동 GC 가 발생합니다. 누수가 있으면 작업 n 회는 반드시 임계값에 도달합니다. 메모리 누수 문제가 수정되었는지 확인하는 데에도 사용할 수 있습니다

P.S. document 수 (iframe 대상), 노드 수, 이벤트 리스너 수, GPU 메모리 점유 변화 추이도 확인할 수 있습니다. 그 중 노드 수와 이벤트 리스너 수 변화도 지도적 의미가 있습니다

Memory

이 패널에는 3 개의 툴이 있습니다: 힙 스냅샷, 메모리 할당 상황, 메모리 할당 타임라인:

  • 힙 스냅샷 (Take Heap Snapshot), 각 타입 객체의 생존 상황을 구체적으로 분석하는 데 사용. 인스턴스 수, 참조 경로 등 포함

  • 메모리 할당 상황 (Record Allocation Profile), 각 함수에 할당된 메모리 크기를 확인하는 데 사용

  • 메모리 할당 타임라인 (Record Allocation Timeline), 메모리의 할당 및 회수의 실시간 상황을 확인하는 데 사용

그 중 메모리 할당 타임라인과 힙 스냅샷이 유용합니다. 타임라인은 메모리 누수 작업의 특정 위치에 사용하���, 스냅샷은 구체적인 문제 분석에 사용합니다

구체적인 사용법에 대한 자세한 내용은 메모리 문제 해결 참조

Record Allocation Timeline

타임라인을 열고, 페이지에 대해 다양한 상호작용 작업을 수행합니다. 파란색 기둥은 새 메모리 할당을 나타내고, 회색은 해제 회수를 나타냅니다. 타임라인上に 규칙적인 파란색 기둥이 존재하면, 메모리 누수가 존재할 가능성이 매우 큽니다

그 후 반복해서 작업하며 관찰하고, 어떤 작업이 파란색 기둥의 잔류를 유발하는지 확인합니다

Take Heap Snapshot

힙 스냅샷은 더 자세한 분석에 사용하며, 누수되고 있는 구체적인 객체 타입을 특정합니다

여기까지 의심스러운 작업을 특정할 수 있어야 합니다. 스냅샷의 각 항목의 수치 변화를 관찰하여 누수 객체 타입을 특정합니다

힙 스냅샷에는 4 가지 표시 모드가 있습니다:

  • Summary: 요약 뷰. 하위 항목을展开하여 Object's retaining tree(참조 경로) 확인

  • Comparison: 비교 뷰. 다른 스냅샷과 비교하여, 증·감·Delta 수치와 메모리 크기 확인

  • Containment: 조망 뷰. 힙을 위에서 아래로 봄. 루트 노드에는 window 객체, GC root, 네이티브 객체 등 포함

  • Dominators: 지배 트리 뷰. 신버전 Chrome 에서는 삭제된 듯. 앞서 용어 개념 부분에서 언급한 지배 트리를 표시

가장 자주 사용되는 것은 비교 뷰와 요약 뷰입니다. 비교 뷰는 2 회 작업과 1 회 작업의 스냅샷을 diff 하여, Delta 증분을 확인하고, 어떤 타입의 객체가 계속 증가하고 있는지 특정합니다. 요약 뷰는 이 의심스러운 객체 타입을 분석하고, Distance 를 확인하고, 이상한 긴 경로상의 어떤 링이 끊어지지 않았는지 특정합니다

요약 뷰를 볼 때의 작은 상식은, 신규 추가는 노란 바탕에 검은 글씨, 삭제는 빨간 바탕에 검은 글씨, 원래 존재한 것은 흰 바탕에 검은 글씨입니다. 이것이 매우 중요합니다

스냅샷 사용법에 대한 자세한 도해는 힙 스냅샷 기록 방법 참조

四.조사 단계

1.문제 확인, 의심스러운 작업 특정

메모리 누수가 존재하는지 먼저 확인:

  1. Performance 패널로 전환, 기록 시작 (처음부터 기록할 필요가 있는 경우)

  2. 기록 시작 -> 작업 -> 기록 중지 -> 분석 -> 반복 확인

  3. 메모리 누수가 존재함을 확인하면, 범위를 좁혀, 어떤 상호작용 작업이 유발하는지 특정

Memory 패널의 메모리 할당 타임라인을 통해 추가로 확인도 가능합니다. Performance 패널의장점은 DOM 노드 수와 이벤트 리스너의 변화 추이를 확인할 수 있다는 점입니다. 성능을 저하시키는 것이 메모리 문제인지 특정하지 못한 경우, Performance 패널을 통해 네트워크 응답 속도, CPU 사용률 등의 요인도 확인할 수 있습니다

2.힙 스냅샷 분석, 의심스러운 객체 특정

의심스러운 상호작용 작업을 특정 후, 메모리 스냅샷을 통해 더 깊이 분석:

  1. Memory 패널로 전환, 스냅샷 1 획득

  2. 의심스러운 상호작용 작업을 1 회 수행, 스냅샷 2 획득

  3. 스냅샷 2 와 1 을 비교, 수치 Delta 가 정상인지 확인

  4. 의심스러운 상호작용 작업을 다시 1 회 수행, 스냅샷 3 획득

  5. 3 와 2 를 비교, 수치 Delta 가 정상인지 확인하고, Delta 이상의 객체 수치 변화 추이 추측

  6. 의심스러운 상호작용 작업을 10 회 수행, 스냅샷 4 획득

  7. 4 와 3 을 비교, 추측을 검증하고, 무엇이 예상대로 회수되지 않는지 특정

3.문제 특정, 원인 특정

의심스러운 객체를 특정 후, 더 문제를 특정:

  1. 해당 타입 객체의 Distance 가 정상인지. 대부분의 인스턴스가 3 급 4 급이고, 개별적으로 10 급 이상은 이상

  2. 경로 깊이 10 급 이상 (또는 명확히 다른 동타입 인스턴스보다 깊은) 의 인스턴스를 확인, 무엇이 그것을 참조하는지

4.참조 해제, 수정 검증

여기까지 기본적으로 문제의 근원을 특정했을 것입니다. 다음으로 문제 해결:

  1. 이 참조를 끊는 방법 생각

  2. 로직 흐름 정리, 다른 곳에서 재사용하지 않을 참조가 존재하는지 확인, 모두 해제

  3. 수정 검증. 해결하지 못한 경우, 재특정

물론, 로직 흐름 정리 는 처음부터 수행할 수도 있습니다. 툴 분석을 사용하면서, 로직 흐름의 취약점을 확인하고, 양쪽 동시에 진행하며, 마지막에 Performance 패널의 추이 꺾은선 또는 Memory 패널의 타임라인으로 검증 가능합니다

五.일반적인 케이스

이러한 시나리오에서는 메모리 누수隐患이 존재할 수 있습니다. 물론, 마무리 작업을 적절히 수행하면 해결 가능합니다

1.암묵적 전역 변수

function foo(arg) {
    bar = "this is a hidden global variable";
}

barwindow 에 걸리고, bar 가 거대한 객체 또는 DOM 노드를 가리키면, 메모리隐患이 됩니다

또 다른 덜 명확한 방법은, 생성자가 직접 호출되는 경우 (new 로 호출하지 않음):

function foo() {
    this.variable = "potential accidental global";
}

// Foo called on its own, this points to the global object (window)
// rather than being undefined.
foo();

또는 익명 함수 내의 this 도, 비엄격 모드에서는 global 을 가리킵니다. lint 검사 또는 엄격 모드를 활성화하여 이러한 명백한 문제를回避할 수 있습니다

2.잊혀진 timer 또는 callback

var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        // Do stuff with node and someResource.
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);

후속으로 idNode 인 노드가 삭제된 경우, 타이머 내의 node 변수는 여전히 그 참조를 보유하여, 유리 DOM 서브트리가 해제되지 않습니다

콜백 함수의 시나리오는 timer 와 유사합니다:

var element = document.getElementById('button');

function onClick(event) {
    element.innerHtml = 'text';
}

element.addEventListener('click', onClick);
// Do stuff
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
// Now when element goes out of scope,
// both element and onClick will be collected even in old browsers that don't
// handle cycles well.

노드를 삭제하기 전에 노드상의 이벤트 리스너를 먼저 제거해야 합니다. IE6 은 DOM 노드와 JS 간의 순환 참조를 처리하지 않기 때문에 (BOM 과 DOM 객체의 GC 전략이 모두 참조 카운트이므로), 메모리 누수가 발생할 수 있습니다. 현대 브라우저에서는 이 필요가 없습니다. 노드가 더 이상 액세스할 수 없으면, 리스너는 회수됩니다

3.유리 DOM 의 참조

var elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image'),
    text: document.getElementById('text')
};

function doStuff() {
    image.src = 'http://some.url/image';
    button.click();
    console.log(text.innerHTML);
    // Much more logic
}

function removeButton() {
    // The button is a direct child of body.
    document.body.removeChild(document.getElementById('button'));

    // At this point, we still have a reference to #button in the global
    // elements dictionary. In other words, the button element is still in
    // memory and cannot be collected by the GC.
}

DOM 노드 참조를 캐시하는 경우가 많습니다 (성능 고려 또는 코드 간결성 고려). 하지만 노드를 삭제할 때, 캐시된 참조를 동기적으로 해제해야 합니다. 그렇지 않으면 유리 서브트리가 해제되지 않습니다

또 다른 더 숨겨진 시나리오:

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 can be #tree GC

아래 그림과 같습니다:

[caption id="attachment_1464" align="alignnone" width="368"]treegc treegc[/caption]

유리 서브트리의 임의의 하나의 노드 참조가 해제되지 않은 경우, 서브트리 전체가 해제할 수 없습니다. 하나의 노드를 통해 다른 모든 노드를 찾을 수 (액세스할 수) 있기 때문에, 모두 활성으로 마크되어, 제거되지 않습니다

4.클로저

var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing)
      console.log("hi");
  };
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log(someMessage);
    }
  };
};
setInterval(replaceThing, 1000);

console 에 붙여넣고 실행한 후, Performance 패널의 추이 꺾은선 또는 Memory 패널의 타임라인으로 메모리 변화를 확인하면, 매우 규칙적인 메모리 누수 (꺾은선이 착실히 상승, 매초 하나의 파란색 기둥이 똑바로) 를 발견할 수 있습니다

클로저의 전형적인 구현 방식은, 각 함수 객체가 사전 객체에 대한 포인터를 가지며, 이 사전 객체가 그 어휘적 작용역을 나타냅니다. replaceThing 에서 정의된 함수가 모두 실제로 originalThing 을 사용하는 경우, 그것들이 모두 같은 객체를 가져야 하므로, originalThing 이 여러 번 재할당되어도, 이러한 (replaceThing 에서 정의된) 함수는동일한 어휘적 환경을 공유합니다

하지만 V8 은 이미 어떤 클로저에도 사용되지 않는 변수를 어휘적 환경에서 제거할 정도로 똑똑하므로, unused 를 제거하거나 (또는 unused 내의 originalThing 액세스를 제거하거나), 메모리 누수를 해결할 수 있습니다

변수가 임의의 클로저에 의해 사용되면, 어휘적 환경에 추가되어, 해당 작용역 하의 모든 클로저에 의해 공유됩니다. 이것이 클로저가 메모리 누수를 유발하는 핵심입니다

P.S. 이 흥미로운 메모리 누수 문제에 대한 자세한 정보는, An interesting kind of JavaScript memory leak 참조

六.기타 메모리 문제

메모리 누수 외에도, 두 가지 일반적인 메모리 문제가 있습니다:

  • 메모리 팽창

  • 빈번한 GC

메모리 팽창은 점유 메모리가 너무 많은 것을 말하지만, 명확한 한계는 없으며, 디바이스마다 성능이 다르므로, 사용자 중심이어야 합니다. 어떤 디바이스가 사용자 층에서 인기 있는지 이해하고, 이러한 디바이스에서 페이지를 테스트합니다. 경험이 매우 나쁘면, 페이지에 메모리 팽창 문제가 존재할 수 있습니다

빈번한 GC 는 경험에 매우 영향을 미칩니다 (페이지가 정지하는 느낌, Stop-The-World 때문에). Task Manager 의 메모리 크기 수치 또는 Performance 추이 꺾은선으로 확인 가능합니다:

  • Task Manager 에서 메모리 또는 JS 메모리 사용량 수치가 빈번하게 상승 하락하면, 빈번한 GC 를 나타냅니다

  • 추이 꺾은선에서, JS 힙 크기 또는 노드 수가 빈번하게 상승 하락하면, 빈번한 GC 가 존재함을 나타냅니다

저장 구조 최적화 (세밀한 입자의 작은 객체 생성 회피), 캐시 재사용 (예: 팩토리 패턴을 사용하여 재사용 구현) 등의 방식으로 빈번한 GC 문제를 해결할 수 있습니다

참고 자료

댓글

아직 댓글이 없습니다

댓글 작성