Weekly Flutter

다트의 가비지 컬렉터 어떻게 동작할까?

플럭 2023. 3. 2. 01:16
해당 글은 공식문서와 chatGPT의 약간의 도움을 받아 작성되었습니다. (고마워 따봉 chatGPT야)

 

플러터 팀에서 초기 언어를 선택할 때 12개 이상의 언어를 선택해 평가하였다고 한다.

플러터는 다트를 선택하였는데, 플러터가 다트를 선택한 주요 이유들 중 하나는 바로

다트가 lock(앱이 실행이 중지되는 상태)없이 가비지 컬렉션을 수행할 수 있다는 점이였다. 

 

많은 언어에서 가비지 컬렉션이 메모리를 수집하는 동안 lock 상태가 된다고 한다. 

하지만 다트는 항상 가비지 컬렉션을 lock 없이 수행할 수 있다는 점에서 플러터 팀의 선택을 받은 것..

 

그럼 다트의 가비지 컬렉터는 어떻게 lock 없이 가비지 컬렉션이 동작을 할까? 

 

Dart의 가비지 컬렉터는 두 개의 세대(generation)로 구성된다.

 

1. Young Space Scavenger

까마귀 같이 썩은걸 먹는 동물들을 Scavenger라고 한다.

일단 첫번째 단계는 Young Space Scavenger로, 이 단계에서는 짧은 수명을 가진 객체들에 대한 메모리를 해제하는 역할을 한다.

Young Space Scavenger는 대개 비교적 짧은 시간동안만 수행되며, 그 성능은 매우 빠르다.

 

Young Space Scavenger의 작동 원리

 

Young Space은 새로운 객체들이 할당되는 메모리 공간을 말한다.

이 영역은 반쪽으로 나눠져 있는데,

하나는 새로운 객체들이 계속해서 할당되는 활성화된 반쪽이고,

다른 하나는 현재 사용되지 않는 비활성화된 반쪽이다.

새로운 객체들은 활성화된 반쪽에 할당되며,

활성화된 반쪽이 가득 차면,

 

이중 살아있는 객체들은 비활성화된 반쪽으로 복사된다.

살아있는지 죽었는지는  활성화된 공간이 가득 차면 그 때 판단하게 된다.

GC(가비지콜렉터)가 루트 객체부터 그것이 무엇을 참조하고 있는지 살펴보며 결정한다.

 

죽었나 살았나 GC가 판단하는 방법 

스택 변수와 같은 루트 개체에서 시작하여 어떤 개체가 아직 사용 중이고 활성 상태인지 확인한다.

이러한 루트 개체가 참조하는 항목과 참조되는 모든 개체는 활성 상태로 간주된 다음 이러한 활성 개체가 참조하는 항목을 검사하고 활성 개체가 참조하는 모든 개체도 활성 상태로 간주된다. 이 프로세스는 도달 가능한 모든 개체가 활성 상태로 표시될 때까지 계속되며 도달할 수 없는 개체는 죽은 것으로 간주되어 재활용될 수 있다. 

그리고 비활성화된 반쪽이 활성화되어서 다시 새로운 객체들이 할당된다.

이런식으로 반복되면서, Young 영역은 계속해서 새로운 객체들을 받아들이는 공간으로 사용된다.

 

Young 영역은 새로운 객체들이 들어올 대기 공간으로서, 빈 자리가 있는 반쪽에 할당하고,

다 차면 기존 객체들을 다른 반쪽으로 옮기는 과정을 반복한다.

 

2. Parallel Marking and Concurrent Sweeping

두 번째 단계에서 수명이 긴 객체들에 대한 메모리를 해제한다.

 

Parallel Marking 단계 : 메모리에서 아직 사용 중인 개체를 식별하는 단계

객체 그래프를 탐색하며, 아직 사용 중인 객체는 표시(mark)한다. 객체 그래프란, 어떤 객체가 다른 객체를 참조하는 구조를 말하는데 즉, 어떤 객체가 다른 객체를 필드나 변수 등으로 참조하고 있는 경우, 이 두 객체는 서로 연결되어 있는 object graph를 형성한다. 

Concurrent Sweeping 단계 : 더 이상 사용하지 않는 메모리를 해제하는 단계

Concurrent Sweeping은  마지막 단계로 Young Space Scavenger와 Parallel Marking 단계 이후, 현재 사용되지 않는 객체들이 어떤 영역을 차지하고 있는지 파악한다. Parallel Marking 단계에서 mark가 없는 객체들이 사용되지 않는 객체로 판단되고 삭제된다.

여기서 주요한 것은 삭제할 객체들은 바로 메모리에서 제거되지 않고, 다시 "힙"이라는 메모리 풀에 돌려보내지며 다음 GC가 실행될 때 다시 재활용된다는 것이다. 이렇게 GC는 메모리 누수를 방지하고 애플리케이션의 성능과 안정성을 향상시킨다.

이 작업은 더 많은 시간이 소요되기 때문에, Garbage Collector는 앱이 사용자와 상호작용하지 않을 때 이 작업을 처리한다. 따라서 lock이 발생되지 않는다. 

 

어떻게 가능할까?

 

앱이 사용자와 상호작용하지 않을 때 이 작업을 처리하는 것이 가능한 이유는 GC가 앱과 UI 성능에 영향을 최소화하기 위해서 Flutter engine에게 hook들을 제공하기 때문이다.

hook

엔진에 의해 앱이 현재 실행되지 않고, 작동하지 않는 상태, 유저 상호작용이 없을 때를 감지해 알려주는 것을 의미한다.

이로써 GC가 성능에 영향을 끼치지 않은채 수집 단계들을 진행할 수 있도록 해준다.

 

Parallel Marking 단계에서는 블록(block)이 발생한다.

하지만 Flutter는 GC 스케줄링을 지원하기 때문에, GC에 의한 영향은 최소화된다.

사용자가 앱을 사용할 때 UI 스레드가 블록되면 앱이 반응하지 않는 것처럼 보일 수 있다.

하지만, Flutter는 이러한 상황을 방지하기 위해 GC 작업을 수행하기 전에 앱이 사용자와 상호작용하지 않는 적절한 시기를 찾아서 작업을 예약한다. 이렇게하면 앱이 블록되지 않고 사용자와의 상호작용이 계속된다.

만약 앱이 weak generational hypothesis을 따르지 않는다면, Concurrent Sweeping 단계가 더 자주 발생할 수 있는데, 이는 Flutter 위젯 구현 방식으로 인해 발생할 가능성은 낮지만 염두에 두면 좋다고 한다.

 

약한 세대 가설(weak generational hypothesis)이란?

대부분의 객체는 생성된 후 짧은 시간 동안만 유효하며 오랫동안 유효한 객체는 일부에 불과하다는 것이다.

대부분의 응용프로그램에서 아래 그래프를 통해 대체적으로 object들이 짧은 시간동안만 살아있단 것을 확인하였고,

weak generational hypothesis이라고 한다.

GC 알고리즘은 메모리를 청소하는 데 있어서 이러한 성질을 이용하여 더 효율적으로 메모리를 관리할 수 있다. 이를 위해 새로 생성된 객체를 'young generation'이라는 영역에 할당하고, 이 영역에서는 매우 짧은 시간 동안만 유효한 객체들이 생성된다.

이후 young generation에서 일정 시간이 지나면 객체들은 'old generation'으로 이동하게 되고, old generation에서는 오랜 기간 동안 사용되는 객체들이 존재하게 된다.

 

참조

https://sungwon-choi-29.github.io/trend/2019-08-16-trend/

https://velog.io/@seunghwanly/%EC%99%9C-Flutter%EA%B0%80-Reactive-Programming%EC%97%90-%EC%A0%81%ED%95%A9%ED%95%9C%EC%A7%80

https://brorica.tistory.com/entry/%EC%9E%90%EB%B0%94-gc