본문으로 건너뛰기

무료로 가비지 컬렉션을 얻다

· 약 7분
Hannes Payer and Ross McIlroy, Idle Garbage Collectors

JavaScript 성능은 Chrome의 핵심 가치 중 하나로, 특히 부드러운 사용자 경험을 제공하는 데 중요한 요소입니다. Chrome 41부터 V8은 웹 애플리케이션의 반응성을 높이기 위해 고비용의 메모리 관리 작업을 사용되지 않는 작은 유휴 시간 조각 안에서 숨기는 새로운 기술을 활용합니다. 그 결과, 가비지 컬렉션으로 인한 끊김 현상이 크게 줄어들면서 웹 개발자는 더 부드러운 스크롤링과 버터 같은 애니메이션을 기대할 수 있습니다.

Chrome의 V8 JavaScript 엔진과 같은 현대적인 언어 엔진은 실행 중인 애플리케이션에 대해 메모리를 동적으로 관리하여 개발자가 이를 직접 신경 쓸 필요가 없게 합니다. 엔진은 주기적으로 애플리케이션에 할당된 메모리를 살펴보고, 더 이상 필요하지 않은 데이터를 확인하여 이를 제거함으로써 공간을 확보합니다. 이 과정은 가비지 컬렉션으로 알려져 있습니다.

Chrome에서는 매끄럽고 초당 60프레임(FPS)의 시각 경험을 제공하기 위해 노력합니다. V8은 이미 작은 청크로 가비지 컬렉션을 수행하려고 하지만, 더 큰 가비지 컬렉션 작업은 예측할 수 없는 시간에, 때로는 애니메이션 중간에 발생하여 실행을 일시 중단시키고 Chrome이 60 FPS 목표에 도달하지 못하게 합니다.

Chrome 41은 Blink 렌더링 엔진용 작업 스케줄러를 포함시켰으며, 이를 통해 지연 시간에 민감한 작업을 우선적으로 처리하여 Chrome이 반응성과 빠른 속도를 유지할 수 있습니다. 작업을 우선 순위화할 수 있을 뿐만 아니라, 이 작업 스케줄러는 시스템이 얼마나 바쁜지, 어떤 작업이 수행되어야 하는지, 각 작업의 긴급성이 어떤지 등에 대한 중앙 집중적인 정보를 보유합니다. 따라서 Chrome이 유휴 상태일 가능성과 얼마나 오래 유휴 상태를 유지할지 대략적으로 추정할 수 있습니다.

웹 페이지에서 애니메이션을 표시할 때를 예로 들어 보겠습니다. 애니메이션은 초당 60프레임으로 화면을 업데이트하며, Chrome에 약 16.6ms의 업데이트 시간을 제공합니다. 따라서 Chrome은 이전 프레임이 화면에 표시되자마자 현재 프레임 작업을 시작해 입력, 애니메이션, 프레임 렌더링 작업을 수행합니다. Chrome이 이 모든 작업을 16.6ms보다 더 짧은 시간에 완료한다면, 다음 프레임 렌더링을 시작할 때까지 남은 시간 동안 할 일이 없습니다. Chrome의 스케줄러는 Chrome이 유휴 상태일 때 특별한 _유휴 작업_을 예약하여 이러한 _유휴 시간_을 V8이 활용할 수 있도록 합니다.

Figure 1: Frame rendering with idle tasks

유휴 작업은 스케줄러가 유휴 시간이라고 판단할 때 실행되는 특별한 낮은 우선 순위의 작업입니다. 유휴 작업에는 스케줄러가 유휴 상태가 얼마나 지속될 것으로 예상하는지에 대한 마감 기한이 주어집니다. Figure 1의 애니메이션 예에서, 이 마감 기한은 다음 프레임이 그려지기 시작해야 하는 시간일 것입니다. 다른 상황(예: 화면 활동이 없는 경우)에서는 다음으로 예약된 작업이 실행될 시간, 혹은 예기치 않은 사용자 입력에 대해 Chrome이 반응성을 유지할 수 있도록 상한선인 50ms가 될 수 있습니다. 이 마감 기한은 유휴 작업이 끊김이나 입력 응답 지연을 유발하지 않고 수행할 수 있는 작업의 양을 추정하는 데 사용됩니다.

유휴 작업에서 수행된 가비지 컬렉션은 중요한 지연 시간에 민감한 작업으로부터 숨겨져 있습니다. 이는 이러한 가비지 컬렉션 작업이 “무료”로 수행된다는 것을 의미합니다. V8이 이를 어떻게 수행하는지 이해하기 위해 V8의 현재 가비지 컬렉션 전략을 검토해 볼 가치가 있습니다.

V8의 가비지 컬렉션 엔진 심층 분석

V8은 세대별 가비지 컬렉터를 사용하며 JavaScript 힙을 새로 할당된 객체를 위한 작은 젊은 세대와 오래 살아남는 객체를 위한 큰 오래된 세대로 나눕니다. 대부분의 객체는 금방 소멸한다는 가설에 따라, 이 세대별 전략은 가비지 컬렉터가 작은 젊은 세대에서 정기적이고 짧은 가비지 컬렉션(소위 스캔지)만 수행하도록 하며, 오래된 세대의 객체를 추적할 필요가 없게 합니다.

젊은 세대는 semi-space 할당 전략을 사용하며, 새 객체는 처음에 젊은 세대의 활성 semi-space에 할당됩니다. 이 semi-space가 꽉 차면 스캐빈지 작업이 살아있는 객체를 다른 semi-space로 이동시킵니다. 한 번 이동된 객체는 오래 사는 것으로 간주되어 오래된 세대로 승격됩니다. 살아있는 객체가 이동되면 새로운 semi-space가 활성화되고, 이전 semi-space의 남아있는 죽은 객체는 폐기됩니다.

따라서 젊은 세대의 스캐빈지 지속 시간은 젊은 세대의 살아있는 객체의 크기에 따라 달라집니다. 대부분의 객체가 젊은 세대에서 접근 불가능하게 되면 스캐빈지는 빠르게 (<1ms) 완료됩니다. 하지만 대부분의 객체가 스캐빈지에서 살아남으면 스캐빈지 지속 시간이 상당히 길어질 수 있습니다.

오래된 세대에서 살아있는 객체가 휴리스틱으로 도출된 제한을 초과하면 전체 힙의 주요 수집 작업이 수행됩니다. 오래된 세대는 지연 시간과 메모리 소비를 개선하기 위한 몇 가지 최적화를 가진 mark-and-sweep collector를 사용합니다. 마킹 지연 시간은 마킹해야 하는 살아있는 객체의 수에 따라 다르며, 대형 웹 애플리케이션의 경우 전체 힙 마킹에 100ms 이상 걸릴 수 있습니다. 이러한 긴 기간 동안 메인 스레드가 멈추는 것을 방지하기 위해 V8은 여러 작은 단계에서 살아있는 객체를 점진적으로 마킹할 수 있는 기능을 오래 전에 도입했으며, 각 마킹 단계가 5ms 이하로 유지되도록 목표하고 있습니다.

마킹 후, 이전 세대 메모리를 스윕하여 애플리케이션에 다시 사용할 수 있는 빈 메모리를 제공합니다. 이 작업은 전용 스위퍼 스레드에 의해 동시에 수행됩니다. 마지막으로, 메모리 세대의 메모리 조각화를 줄이기 위해 메모리 압축이 수행됩니다. 이 작업은 시간이 많이 걸릴 수 있으며, 메모리 조각화가 문제가 되는 경우에만 수행됩니다.

요약하면, 주요 가비지 컬렉션 작업은 다음 네 가지입니다:

  1. 젊은 세대 스캐빈지, 보통 빠르게 완료됩니다
  2. 단계 크기에 따라 임의로 길어질 수 있는 점진적 마커에 의한 마킹 단계
  3. 오래 걸릴 수 있는 전체 가비지 컬렉션
  4. 메모리 조각화를 정리하기 위한 공격적인 메모리 압축을 포함한 전체 가비지 컬렉션

이러한 작업을 유휴 기간 동안 수행하기 위해 V8은 가비지 컬렉션 유휴 작업을 스케줄러에 게시합니다. 이 유휴 작업이 실행될 때는 작업 완료 기한이 제공됩니다. V8의 가비지 컬렉션 유휴 시간 처리기는 메모리 소비를 줄이기 위해 어떤 가비지 컬렉션 작업을 수행할지 평가하며, 렌더링 프레임이나 입력 지연에서 미래의 끊김 현상을 방지하기 위해 기한을 존중합니다.

애플리케이션의 측정된 할당 속도가 젊은 세대가 다음 예상 유휴 기간 전에 꽉 찰 수 있음을 보여주는 경우, 유휴 작업 동안 가비지 컬렉터는 젊은 세대 스캐빈지를 수행합니다. 또한 최근 스캐빈지 작업에 소요된 평균 시간을 계산하여 미래의 스캐빈지 지속 시간을 예측하고 유휴 작업 기한을 위반하지 않도록 합니다.

오래된 세대에서 살아있는 객체 크기가 힙 한계에 근접하면 점진적 마킹이 시작됩니다. 점진적 마킹 단계는 마킹해야 하는 바이트 수에 따라 선형적으로 조정될 수 있습니다. 평균적으로 측정된 마킹 속도를 기준으로 가비지 컬렉션 유휴 시간 처리기는 주어진 유휴 작업에 가능한 많은 마킹 작업을 맞추려고 합니다.

오래된 세대가 거의 가득 차 있고 작업에 제공된 기한이 컬렉션을 완료하기에 충분히 길다고 예상되는 경우, 유휴 작업 동안 전체 가비지 컬렉션이 예약됩니다. 컬렉션 멈춤 시간은 마킹 속도와 할당된 객체 수에 따라 예측됩니다. 추가 압축이 포함된 전체 가비지 컬렉션은 웹페이지가 상당한 시간 동안 유휴 상태일 때만 수행됩니다.

성능 평가

유휴 시간 동안 가비지 컬렉션을 실행하는 영향을 평가하기 위해 Chrome의 텔레메트리 성능 벤치마킹 프레임워크를 사용하여 인기 있는 웹사이트들이 로드되는 동안 얼마나 매끄럽게 스크롤되는지 평가했습니다. Linux 워크스테이션에서 상위 25개 사이트와 Android Nexus 6 스마트폰에서 일반적인 모바일 사이트를 벤치마킹하였으며, 두 경우 모두 인기 있는 웹페이지(예: Gmail, Google Docs, YouTube 같은 복잡한 웹앱 포함)를 열고 콘텐츠를 몇 초 동안 스크롤합니다. Chrome은 매끄러운 사용자 경험을 위해 60FPS에서 스크롤을 유지하려고 합니다.

그림 2는 유휴 시간 동안 예약된 가비지 컬렉션 비율을 보여줍니다. 워크스테이션의 더 빠른 하드웨어는 Nexus 6에 비해 더 많은 전체 유휴 시간을 제공하며, 결과적으로 유휴 시간 동안 예약된 가비지 컬렉션 비율이 더 높아지게 되었고(Nexus 6의 31%에 비해 43%) 이는 jank 메트릭의 약 7% 개선을 가져왔습니다.

그림 2: 유휴 시간 동안 발생하는 쓰레기 수집 비율

페이지 렌더링의 매끄러움을 개선하는 것뿐만 아니라, 이러한 유휴 시간은 페이지가 완전히 유휴 상태가 되었을 때 더 적극적인 쓰레기 수집을 수행할 기회를 제공합니다. Chrome 45의 최근 개선 사항은 이를 활용하여 유휴 상태의 포그라운드 탭이 소비하는 메모리 양을 획기적으로 줄입니다. 그림 3은 Gmail의 JavaScript 힙 메모리 사용량이 유휴 상태가 되었을 때, Chrome 43을 사용할 때와 비교하여 약 45% 감소할 수 있는 방법을 미리 보여줍니다.

그림 3: Chrome 43과 비교했을 때 Gmail의 최신 Chrome 45 버전에서의 메모리 사용량

이러한 개선 사항들은 비용이 많이 드는 쓰레기 수집 작업이 수행되는 시기를 더 스마트하게 조정함으로써 쓰레기 수집 중단을 숨길 수 있다는 것을 보여줍니다. 웹 개발자들은 이제 매끄러운 60 FPS 애니메이션을 목표로 하면서도 쓰레기 수집 중단을 두려워하지 않아도 됩니다. 쓰레기 수집 일정 조정을 더욱 확장하기 위한 추가 개선 사항이 있으니 기대해 주세요.