Gmail 규모의 효과적인 메모리 관리

John McCutchan
John McCutchan
Loreena Lee
Loreena Lee

소개

JavaScript는 자동 메모리 관리를 위해 가비지 컬렉션을 사용하지만, 이는 애플리케이션에서의 효과적인 메모리 관리를 대체하지는 않습니다. JavaScript 애플리케이션은 메모리 누수 및 팽창과 같은 네이티브 애플리케이션과 동일한 메모리 관련 문제를 겪지만 가비지 컬렉션 일시중지도 처리해야 합니다. Gmail과 같은 대규모 애플리케이션은 소규모 애플리케이션과 동일한 문제를 겪습니다. Gmail팀이 Chrome DevTools를 사용하여 메모리 문제를 파악, 격리, 해결한 방법을 알아보세요.

Google I/O 2013 세션

이 자료는 Google I/O 2013에서 발표되었습니다. 아래 동영상을 확인하세요.

Gmail, 문제가 있습니다.

Gmail팀에 심각한 문제가 발생했습니다. 리소스가 제한된 노트북과 데스크톱에서 Gmail 탭이 여러 기가바이트의 메모리를 소비한다는 사용자의 경험담이 점점 더 자주 접수되었으며, 종종 전체 브라우저가 다운된다는 결론을 내렸습니다. CPU가 100%로 고정되고, 앱이 응답하지 않으며, Chrome 탭이 비활성화되는 ('죽었어, 짐') 이야기가 있습니다. 팀은 문제를 해결하는 것은 물론 문제를 진단하는 것조차 어떻게 시작해야 할지 몰랐습니다. 이 문제의 확산 정도를 파악할 수 없었고 사용 가능한 도구가 대규모 애플리케이션으로 확장되지 않았습니다. 이 팀은 Chrome팀과 협력하여 메모리 문제를 분류하는 새로운 기법을 개발하고 기존 도구를 개선했으며 현장에서 메모리 데이터를 수집할 수 있도록 했습니다. 하지만 도구를 살펴보기 전에 JavaScript 메모리 관리의 기본사항을 알아보겠습니다.

메모리 관리 기본사항

JavaScript에서 메모리를 효과적으로 관리하려면 먼저 기본사항을 이해해야 합니다. 이 섹션에서는 기본 유형, 객체 그래프를 다루고 일반적인 메모리 팽창 및 JavaScript의 메모리 누수에 대한 정의를 제공합니다. JavaScript의 메모리는 그래프로 개념화할 수 있으므로 그래프 이론이 JavaScript 메모리 관리 및 힙 프로파일러에 중요한 역할을 합니다.

기본 유형

JavaScript에는 세 가지 기본 유형이 있습니다.

  1. 숫자 (예: 4, 3.14159)
  2. 불리언(true 또는 false)
  3. 문자열 ('Hello World')

이러한 기본 유형은 다른 값을 참조할 수 없습니다. 객체 그래프에서 이러한 값은 항상 리프 또는 말단 노드이므로 나가는 가장자리가 없습니다.

컨테이너 유형은 객체 하나뿐입니다. JavaScript에서 객체는 연결 배열입니다. 비어 있지 않은 객체는 다른 값 (노드)으로 향하는 출력 가장자리가 있는 내부 노드입니다.

배열은 어떨까요?

JavaScript의 배열은 실제로 숫자 키가 있는 객체입니다. 이는 단순화된 표현입니다. JavaScript 런타임은 배열과 유사한 객체를 최적화하고 배열로 내부적으로 표현하기 때문입니다.

용어

  1. 값 - 기본 유형, 객체, 배열 등의 인스턴스
  2. 변수 - 값을 참조하는 이름입니다.
  3. 속성 - 값을 참조하는 객체의 이름입니다.

객체 그래프

JavaScript의 모든 값은 객체 그래프의 일부입니다. 그래프는 루트(예: window 객체)로 시작합니다. GC 루트는 브라우저에서 생성되고 페이지가 언로드될 때 소멸되므로 개발자가 GC 루트의 전체 기간을 관리할 수는 없습니다. 전역 변수는 실제로 창의 속성입니다.

객체 그래프

값이 가비지가 되는 경우

루트에서 값으로의 경로가 없으면 값이 가비지가 됩니다. 즉, 루트에서 시작하여 스택 프레임에 있는 모든 객체 속성과 변수를 철저히 검색해도 값에 도달할 수 없으며 가비지가 된 것입니다.

가비지 그래프

JavaScript의 메모리 누수란 무엇인가요?

JavaScript의 메모리 누수는 페이지의 DOM 트리에서 연결할 수 없지만 여전히 JavaScript 객체에서 참조하는 DOM 노드가 있는 경우에 가장 자주 발생합니다. 최신 브라우저에서는 실수로 유출을 일으키기가 점점 더 어려워지고 있지만 생각보다 쉽습니다. 다음과 같이 DOM 트리에 요소를 추가한다고 가정해 보겠습니다.

email.message = document.createElement("div");
displayList.appendChild(email.message);

나중에 표시 목록에서 요소를 삭제합니다.

displayList.removeAllChildren();

email가 존재하는 한 메시지에서 참조하는 DOM 요소는 페이지의 DOM 트리에서 분리되더라도 삭제되지 않습니다.

불안정이란 무엇인가요?

페이지가 최적의 페이지 속도를 위해 필요한 것보다 더 많은 메모리를 사용하면 페이지가 팽창합니다. 메모리 누수도 간접적으로 확장 소모를 일으키지만 이는 의도된 작동이 아닙니다. 크기 제한이 없는 애플리케이션 캐시는 메모리 팽창의 일반적인 원인입니다. 또한 이미지에서 로드된 픽셀 데이터와 같은 호스트 데이터로 인해 페이지가 확장될 수 있습니다.

가비지 컬렉션이란 무엇인가요?

가비지 컬렉션은 JavaScript에서 메모리를 회수하는 방법입니다. 이 작업이 언제 발생할지는 브라우저가 결정합니다. 수집 중에는 GC 루트에서 시작하여 객체 그래프를 탐색하여 실시간 값을 찾는 동안 페이지의 모든 스크립트 실행이 정지됩니다. 연결할 수 없는 모든 값은 가비지로 분류됩니다. 가비지 값의 메모리는 메모리 관리자에 의해 회수됩니다.

V8 가비지 컬렉터 세부정보

가비지 컬렉션이 실행되는 방식을 더 잘 이해하려면 V8 가비지 컬렉터를 자세히 살펴보겠습니다. V8은 세대별 수집기를 사용합니다. 메모리는 새 메모리와 오래된 메모리 세대로 나뉩니다. 영 세대 내에서 할당 및 수집은 빠르고 빈번하게 이루어집니다. 이전 세대 내의 할당 및 수집은 속도가 느리고 빈도가 낮습니다.

세대별 수집기

V8은 2세대 수집기를 사용합니다. 값의 연령은 할당된 이후 할당된 바이트 수로 정의됩니다. 실제로 값의 연령은 살아남은 영 세대 수집 횟수로 근사하는 경우가 많습니다. 값이 충분히 오래되면 이전 세대에 테넌트됩니다.

실제로 새로 할당된 값은 오래 유지되지 않습니다. Smalltalk 프로그램에 대한 연구에 따르면, 젊은 세대 수집 후에는 값의 7% 만 유지됩니다. 런타임 전반에서 진행된 유사한 연구에 따르면 새로 할당된 값의 평균 90~70%가 이전 세대에 테넌트되지 않는 것으로 나타났습니다.

젊은 세대

V8의 영 세대 힙은 from 및 to라는 두 공간으로 나뉩니다. 메모리는 to 공간에서 할당됩니다. 할당은 매우 빠르며, to 공간이 가득 차면 young 세대 컬렉션이 트리거됩니다. 영 세대 수집은 먼저 출발 공간과 도착 공간을 전환하고, 이전 도착 공간 (이제 출발 공간)을 스캔하고, 모든 실시간 값을 도착 공간에 복사하거나 이전 세대에 테넌트합니다. 일반적인 영 세대 수집은 약 10밀리초 (ms)가 소요됩니다.

직관적으로 애플리케이션에서 할당할 때마다 to 공간이 고갈되고 GC 일시중지가 발생한다는 것을 이해해야 합니다. 게임 개발자 참고사항: 16ms 프레임 시간 (초당 60프레임 달성에 필요)을 보장하려면 애플리케이션에서 할당을 0으로 해야 합니다. 단일 영의 세대 컬렉션이 프레임 시간의 대부분을 차지하기 때문입니다.

영 세대 힙

이전 세대

V8의 이전 세대 힙은 수집에 마크-컴팩트 알고리즘을 사용합니다. 이전 세대 할당은 값이 영 세대에서 올드 세대로 테넌트될 때마다 발생합니다. 이전 세대 수집이 발생할 때마다 새 세대 수집도 실행됩니다. 애플리케이션이 몇 초 내에 일시중지됩니다. 실제로는 이전 세대 컬렉션이 드물기 때문에 허용됩니다.

V8 GC 요약

가비지 컬렉션을 사용한 자동 메모리 관리는 개발자 생산성에 좋지만 값을 할당할 때마다 가비지 컬렉션 일시중지에 점점 더 가까워집니다. 가비지 컬렉션 일시중지는 버벅거림을 유도하여 애플리케이션의 느낌을 망칠 수 있습니다. 이제 JavaScript가 메모리를 관리하는 방법을 이해했으므로 애플리케이션에 적합한 선택을 할 수 있습니다.

Gmail 수정

지난 1년 동안 Chrome DevTools에 다양한 기능과 버그 수정이 적용되어 그 어느 때보다 강력해졌습니다. 또한 브라우저 자체에서 performance.memory API를 주요 변경하여 Gmail 및 기타 애플리케이션이 필드에서 메모리 통계를 수집할 수 있도록 했습니다. 이러한 멋진 도구를 사용해 한때 불가능해 보였던 작업이 곧 범인을 추적하는 흥미로운 게임이 되었습니다.

도구 및 기법

필드 데이터 및 performance.memory API

Chrome 22부터 performance.memory API가 기본적으로 사용 설정됩니다. Gmail과 같이 장기 실행되는 애플리케이션의 경우 실제 사용자의 데이터가 매우 중요합니다. 이 정보를 통해 Google은 하루에 8~16시간 동안 Gmail을 사용하고 하루에 수백 개의 메일을 받는 헤비 사용자와 하루에 몇 분 동안 Gmail을 사용하고 일주일에 12개 정도의 메일을 받는 일반 사용자를 구분할 수 있습니다.

이 API는 다음 세 가지 데이터를 반환합니다.

  1. jsHeapSizeLimit - JavaScript 힙이 제한되는 메모리 양 (바이트)입니다.
  2. totalJSHeapSize - JavaScript 힙에 할당된 메모리 양 (바이트)으로, 여유 공간을 포함합니다.
  3. usedJSHeapSize - 현재 사용 중인 메모리 양 (바이트)입니다.

API는 전체 Chrome 프로세스의 메모리 값을 반환한다는 점에 유의하세요. 기본 모드는 아니지만 특정 상황에서 Chrome은 동일한 렌더러 프로세스에서 여러 탭을 열 수 있습니다. 즉, performance.memory에서 반환된 값에는 앱이 포함된 탭 외에도 다른 브라우저 탭의 메모리 사용량이 포함될 수 있습니다.

대규모 메모리 측정

Gmail은 JavaScript를 계측하여 performance.memory API를 사용하여 약 30분마다 한 번 메모리 정보를 수집했습니다. 많은 Gmail 사용자가 앱을 며칠 동안 계속 실행하기 때문에 팀은 시간 경과에 따른 메모리 증가와 전반적인 메모리 사용량 통계를 추적할 수 있었습니다. 무작위로 선정된 사용자로부터 메모리 정보를 수집하도록 Gmail을 계측한 지 며칠 만에 팀은 평균적인 사용자 사이에서 메모리 문제가 얼마나 널리 퍼져 있는지 파악할 수 있는 충분한 데이터를 확보했습니다. 기준을 설정하고 들어오는 데이터 스트림을 사용하여 메모리 사용량 감소라는 목표를 향한 진행 상황을 추적했습니다. 결국 이 데이터는 메모리 회귀를 포착하는 데도 사용됩니다.

추적 목적 외에도 현장 측정은 메모리 사용량과 애플리케이션 성능 간의 상관관계에 대한 유용한 정보를 제공합니다. '메모리가 많을수록 성능이 향상된다'는 일반적인 생각과 달리 Gmail팀은 메모리 사용량이 클수록 일반적인 Gmail 작업의 지연 시간이 길어진다는 사실을 발견했습니다. 이 정보를 바탕으로 그 어느 때보다도 메모리 사용량을 줄이기 위해 노력했습니다.

대규모 메모리 측정

DevTools 타임라인으로 메모리 문제 식별

성능 문제를 해결하는 첫 번째 단계는 문제가 있음을 증명하고, 재현 가능한 테스트를 만들고, 문제의 기준 측정을 수행하는 것입니다. 재현 가능한 프로그램이 없으면 문제를 안정적으로 측정할 수 없습니다. 기준 측정이 없으면 실적이 얼마나 개선되었는지 알 수 없습니다.

DevTools 타임라인 패널은 문제가 있음을 증명하는 데 이상적입니다. 웹 앱 또는 페이지를 로드하고 상호작용할 때 시간이 어디에 소비되는지 전체적으로 파악할 수 있습니다. 리소스 로드에서 JavaScript 파싱, 스타일 계산, 가비지 컬렉션 일시중지, 다시 칠하기에 이르기까지 모든 이벤트가 타임라인에 표시됩니다. 메모리 문제를 조사하기 위해 타임라인 패널에는 총 할당된 메모리, DOM 노드 수, 창 객체 수, 할당된 이벤트 리스너 수를 추적하는 메모리 모드도 있습니다.

문제의 존재 입증

먼저 메모리가 누수되는 것으로 의심되는 일련의 작업을 식별합니다. 타임라인 녹화를 시작하고 일련의 작업을 실행합니다. 하단의 휴지통 버튼을 사용하여 전체 가비지 컬렉션을 강제 실행합니다. 몇 번 반복한 후 톱니바퀴 모양의 그래프가 표시되면 수명이 짧은 객체를 많이 할당하고 있는 것입니다. 그러나 일련의 작업으로 인해 메모리가 유지되지 않을 것으로 예상되는데 DOM 노드 수가 시작점의 기준점으로 다시 떨어지지 않으면 누수가 있다고 의심할 만한 근거가 충분합니다.

톱니 모양 그래프

문제가 있는지 확인한 후 DevTools 힙 프로파일러에서 문제의 원인을 파악하는 데 도움을 받을 수 있습니다.

DevTools 힙 프로파일러로 메모리 누수 찾기

Profiler 패널은 CPU 프로파일러와 힙 프로파일러를 모두 제공합니다. 힙 프로파일링은 객체 그래프의 스냅샷을 찍는 방식으로 작동합니다. 스냅샷을 찍기 전에 새 세대와 이전 세대가 모두 가비지 컬렉션됩니다. 즉, 스냅샷이 찍혔을 때 유효했던 값만 표시됩니다.

힙 프로파일러에는 이 도움말에서 충분히 다루기에는 너무 많은 기능이 있지만 Chrome 개발자 사이트에서 자세한 문서를 확인할 수 있습니다. 여기서는 힙 할당 프로파일러에 중점을 둘 것입니다.

힙 할당 프로파일러 사용

힙 할당 프로파일러는 힙 프로파일러의 세부적인 스냅샷 정보를 타임라인 패널의 증분 업데이트 및 추적과 결합합니다. 프로필 패널을 열고 Record Heap Allocations 프로필을 시작하고 일련의 작업을 실행한 다음 분석을 위해 기록을 중지합니다. 할당 프로파일러는 녹화 중 정기적으로 힙 스냅샷을 촬영하며 (매 50ms마다 촬영!) 녹화가 끝날 때 마지막으로 최종 스냅샷을 하나 촬영합니다.

힙 할당 프로파일러

맨 위의 막대는 새 객체가 힙에서 발견되는 시점을 나타냅니다. 각 막대의 높이는 최근 할당된 객체의 크기에 해당하며 막대의 색상은 이러한 객체가 최종 힙 스냅샷에 여전히 있는지 여부를 나타냅니다. 파란색 막대는 타임라인 끝에 여전히 활성 상태인 객체를 나타내고 회색 막대는 타임라인 중에 할당되었지만 이후 가비지 컬렉션된 객체를 나타냅니다.

위의 예에서는 작업이 10번 실행되었습니다. 샘플 프로그램이 객체를 다섯 개 캐시하므로 마지막 다섯 개의 파란색 막대를 예상할 수 있습니다. 하지만 맨 왼쪽의 파란색 막대는 문제가 발생할 가능성을 나타냅니다. 그런 다음 위의 타임라인에 있는 슬라이더를 사용하여 특정 스냅샷을 확대한 다음, 그 시점에 최근 할당된 객체들을 확인할 수 있습니다. 힙에서 특정 객체를 클릭하면 힙 스냅샷 하단 부분에 해당 객체의 보존 트리가 표시됩니다. 객체에 대한 보존 경로를 검사해보면 객체가 수집되지 않은 이유를 이해할 수 있을 정도로 충분한 정보를 확보할 수 있으며, 따라서 필요한 경우 코드를 변경하여 불필요한 참조를 제거할 수 있습니다.

Gmail의 메모리 문제 해결

위에서 설명한 도구와 기법을 사용하여 Gmail팀은 몇 가지 버그 카테고리를 식별할 수 있었습니다. 여기에는 무제한 캐시, 실제로는 발생하지 않는 일을 기다리는 무한히 늘어나는 콜백 배열, 의도치 않게 타겟을 유지하는 이벤트 리스너가 포함됩니다. 이러한 문제를 해결하여 Gmail의 전체 메모리 사용량이 크게 줄었습니다. 99% 의 사용자는 이전보다 80% 적은 메모리를 사용했으며 중간 사용자의 메모리 사용량은 거의 50% 감소했습니다.

Gmail 메모리 사용량

Gmail에서 메모리를 덜 사용하게 되어 GC 일시중지 지연 시간이 줄어들고 전반적인 사용자 환경이 개선되었습니다.

또한 Gmail팀에서 메모리 사용량에 관한 통계를 수집하여 Chrome 내에서 가비지 컬렉션 회귀를 발견할 수 있었습니다. 특히 Gmail의 메모리 데이터에 할당된 총 메모리와 실제 메모리 간의 격차가 급격히 증가하기 시작하면서 두 가지 단편화 버그가 발견되었습니다.

액션 유도하기

다음 질문을 스스로 던져 보세요.

  1. 앱에서 사용 중인 메모리는 얼마인가요? 메모리를 너무 많이 사용하고 있을 수 있으며, 이는 일반적인 생각과 달리 전반적인 애플리케이션 성능에 부정적인 영향을 미칩니다. 정확한 숫자를 알기는 어렵지만 페이지에서 사용하는 추가 캐싱이 측정 가능한 성능 영향을 미치는지 확인해야 합니다.
  2. 내 페이지에 유출된 정보가 없나요? 페이지에 메모리 누수가 발생하면 페이지의 성능뿐만 아니라 다른 탭에도 영향을 미칠 수 있습니다. 객체 추적기를 사용하여 누수를 파악합니다.
  3. 페이지가 얼마나 자주 GC를 실행하나요? Chrome 개발자 도구타임라인 패널을 사용하여 GC 일시중지를 확인할 수 있습니다. 페이지가 자주 GC되는 경우 너무 자주 할당하여 초기 세대 메모리를 소모하고 있을 가능성이 높습니다.

결론

YouTube는 위기 속에서 시작되었습니다. 특히 JavaScript 및 V8의 메모리 관리에 관한 핵심 기본사항을 다뤘습니다. Chrome의 최신 빌드에서 사용할 수 있는 새로운 객체 추적기 기능을 비롯한 도구를 사용하는 방법을 알아봤습니다. Gmail팀은 이 지식을 바탕으로 메모리 사용량 문제를 해결하고 성능을 개선했습니다. 웹 앱에서도 동일한 작업을 할 수 있습니다.