Hwan'Story › @kim-hwan

플러터의 가비지 컬렉터

· (수정 ) · 조회 1468

개요

플러터에서는 Dart 를 개발 언어로 사용하고 런타임으로도 사용한다. 이 글에서는 Dart 런타임의 Garbage Collector 에 대해서 알아본다.

Dart 런타임의 가비지 컬렉터

Dart의 런타임에는 2세대로 구성된 가비지 컬렉터가 포함되어 있다.

  • Young Space Scavenger
  • Parallel Mark Sweep Collectors

Scheduling

활발히 실행 중인 앱에서 가비지 컬렉터의 동작이 발생하게 되면 사용자는 UI의 끊김 등의 좋지 않은 경험을 할 수 있다. 다트의 가비지 컬렉터는 이러한 현상을 최소화 하기 위해서 앱이 Idle 상태이고 사용자와 상호 작용이 없는지 감시하는 Hook을 플러터 엔진에 제공한다. 이를 통해 최대한 앱의 성능에 영향을 주지 않고 가비지 컬렉팅을 할 수 있는 기회를 얻게 된다.

Sliding Compaction 앱이 Idle 상태이고 사용자와 상호 작용이 없는 상태일 때 Sliding Compaction 도 일어나게 되는데 이 동작은 정리 대상이 아닌 활성 객체들을 연속된 메모리 공간에 차례로 쌓는 동작을 수행하여 파편화를 줄여 메모리 효율성을 높힌다.

Young Space Scavenger

이 단계의 경우에는 수명이 짧은 객체들을 정리하기 위해 설계되었다. 이 단계는 아래에서 설명할 Mark/Sweep 동작보다 훨씬 빠르고 위에서 설명한 스케줄링과 결합하면 가비지 컬렉터 동작 중 앱이 버벅이는 증상을 거의 제거할 수 있다.

1_pNaeZ0l8oMCP-f1UUs-V1g.webp

  1. 새 객체가 할당되는 공간은 "세미 스페이스" 라고 부르는 공간에 할당이 되는데 이 공간은 두 개로 나누어진다. 이 두 개의 공간은 한쪽은 "활성", 다른 한쪽은 "비활성" 상태이고 항상 한 쪽만 사용되게 된다.
  2. 새 객체를 할당해야 되면 마지막으로 할당했던 객체의 다음에 연속적으로 할당되게 된다. (활성영역)
    • 이러한 할당 방법을 Bump Pointer Allocation 이라고 하는데 그냥 단순히 마지막 할당했던 객체의 위치와 사이즈만 가지고 있다가 새로 할당할 객체가 생기면 해당 정보를 이용해서 그냥 다음 위치에 할당하는 방법이다, 장점은 매우 빠른 속도이다. O(1) 시간복잡도
  3. 활성 영역이 가득 찰 때까지 연속적인 할당이 진행된다.
  4. 활성 영역이 가득 찬 경우 아직 살아있는 객체들을 찾아낸다.
  5. 살아있는 객체들을 "비활성" 공간으로 옮긴다.
  6. "비활성" 이었던 공간을 "활성" 상태로 변경하고 "활성" 상태였던 공간은 "비활성" 상태로 변경한다.

위와 같은 동작이 반복되게 된다.

Bump Pointer Allocation 과 Isolates

Dart는 기본적으로 싱글 스레드 환경이다. 그렇다고 Dart에서 스레드를 추가로 만들 수 없는 건 아니다. Isolates라 불리는 별도의 스레드를 만들 수 있는데 멀티 스레드가 가능한 다른 언어와는 다른 특징을 가지고 있다. Isolates 는 Dart 코드가 동작하는 고유의 Event Loop 와 메모리 공간을 가진다.

image.png

따라서 Dart는 스레드 간 동기화를 위한 별도의 작업이 필요가 없다. 이러한 특징에는 Bump Pointer Allocation 동작에 큰 장점으로 작용하는데 그 이유는 메모리를 공유하는 멀티 스레드 환경에서 Bump Pointer Allocation 을 위해서는 할당한 마지막 객체의 위치와 사이즈를 기록하기 위해 Lock을 걸어야 하고, 그에 따라 속도가 느려진다. 하지만 Dart에서는 각 스레드(Isolates) 가 별도의 메모리 공간을 가지고 있기 때문에 Lock을 걸 필요가 없다.

(물론 장점만 있는 건 아니다. 각 Isolates 별로 별도의 메모리 공간을 가지고 있기 때문에 많은 수의 Isolates 가 메모리 영역만 할당을 받고 사용하지 않으면 메모리 파편화의 원인이 될 수 있다)

Parallel Marking and Concurrent Sweeping

객체의 수명이 일정 수준에 도달하면 2세대 가비지 컬렉터인 Mark-Sweep 에 의해서 관리되는 새로운 메모리 공간으로 옮겨지게 된다. 이름에서 알 수 있듯이 Mark (사용중인 객체로 표시하는 작업), Sweep (표시되지 않은 객체를 제거하는 작업) 이 일어나게 된다. 이 때 Mark 작업을 할 때 앱이 Block 상태가 되기 때문에 UI 스레드가 멈추게 된다.

이러한 형태 (Mark-Sweep) 의 동작이 많이 일어나게 되면 앱이 버벅이거나 멈추는 등 좋지 못한 경험을 사용자에게 줄 수 있기 때문에 유의해야 한다. (플러터에서 위젯을 구현하는 방법을 보았을 때 자주 일어날만한 상황은 아니지만 염두해 두어야 한다.)

정리

플러터에서 왜 Dart 언어를 선택했는지 궁금하여 찾아본 자료였는데 플러터에서의 앱 구현 방식과 다트 런타임의 동작이 어울린다고 생각된다.

참고사이트

Flutter: Don’t Fear the Garbage Collector

Dart SDK Doc

Cheney's algorithm

Generational GC

관련 게시글