[세미나] 각 언어별 가비지 컬렉션의 동작 방식
오늘 제가 이야기해 볼 주제는 가비지 컬렉터 입니다. 개발일을 하면서 가비지 컬렉터에 대해 많이 들어보셨을 거고, 대충 어떤 일을 하는 애 인지 알고 계실거에요. 가비지 컬렉터는 쉽게 말해 메모리에 굳이 남아있을 필요없는 쓰레기들을 주워가는 쓰레기 콜렉터죠. 우리가 쓰는 언어들 대부분 가비지 컬렉터가 구현되어 있어서 굳이 이걸 왜 알아야할까 싶기도 합니다. 우리가 가비지 컬렉터를 알아야 하는 이유는 명확합니다.가비지 컬렉터의 작동 방식을 이해하면 메모리 누수나 성능 저하, 오버플로우와 같은 문제를 미리 예방할 수 있기 때문입니다.
메모리 관리에 소홀하게 되면 정말 크나큰 문제가 생기기도 하는데, 극단적인 사례가 하나있습니다. 1996년 '아리안 5'라는 로켓 하나가 폭발하게 됩니다. 유럽도 인간을 우주에 보낼 수 있다는 꿈을 안고 프랑스를 주축으로 유럽 12개국이 약 6조 7천억 원을 투자해 로켓하나를 쏩니다. 하지만 40초도 안돼서 꿈이 산산조각 나버리게 됩니다. 손해액과 자존심에 금이 간 것에 비해서 원인은 참 간단했습니다. 프로그래머들이 아리안 4호 코드를 복붙했기 때문인데요, 당시 학자들은 아리안 4호의 속도가 16비트 정수형의 최댓값(32,767)을 넘지못할 것이라고 예측하고 코드를 작성했는데 아리안 5호에도 똑같은 코드를 복붙해버렸죠. 이 값은 로켓의 방향제어 시스템에서 사용되었는데, 이렇게 저장한 값이 너무 크게 되어버려서 오버플로우가 났고, 결과적으로 로켓은 날아가다가 폭발해버렸습니다.
이 사건의 문제는 우리에게 다양한 가르침을 주죠. 그 중 하나의 큰 깨달음은 개발자 자신이 작성한 코드가 메모리를 어떻게 사용하는지 정확히 이해하고 있어야 한다는거죠. 메모리 해제를 철저하게 수행하면 시스템의 안정성을 확보할 수 있고, 이를 통해 이런 심각한 사고를 방지할 수도 있겠죠. 따라서 가비지 컬렉터의 방식을 이해하고 적절히 활용할 수 있어야합니다.
이 가비지 컬렉터가 어디에 존재하는지부터 살펴보도록 하겠습니다. 우리가 쓰고 있는 대부분의 언어는 매니지드 언어라고 불리죠. 흔히 매니지드 언어, 매니지드 코드라고 하는 건 쉽게 말해 언어에 런타임 환경이 더해진 언어인데요. 언매니지드 언어는 하드웨어에서 직접 실행될 수 있지만 매니지드 언어는 런타임 환경에 의존적인 코드입니다.
런타임 환경은 개발자를 위해 많은 서비스를 제공해주는데, 이 중 가장 대표적인 것이 가비지 콜렉션 입니다.
가비지가 수집되는 과정은 두가지 쓰레드로 나눠볼 수가 있습니다. 하나는 뮤테이터고 하나는 콜렉터입니다.
뮤테이터는 객체를 할당하고 참조에 따라 그래프를 변형시키는 역할을 합니다. 즉 쉽게말해 뮤테이터는 우리가 작성하는 코드 그 자체라고 할 수 있습니다. 따라서 우리가 작성하는 코드이기 때문에 참조되고 변화하는 이 과정은 잘 이해하고 있죠. 이 콜렉터는 뮤테이터 쓰레드에서 접근할 수 없는 쓰레기 메모리가 된 것들을 찾아 저장공간을 회수하는 역할을 합니다. 실질적인 가비지 컬렉터라고 할 수 있는 해당 쓰레드 과정의 알고리즘은 정확히 알기가 어렵죠.
그래서 오늘은 이 콜렉터 쓰레드에서 일어나는 대표적인 알고리즘을 세가지인 레퍼런스 카운팅, 마크앤 스윕, 제너레이션 가비지 컬렉션에 대해 알아보고, 각 언어들이 어떤 방식을 채택하고 있는지, 어떤 장단점이 있는지 알아보는 시간을 갖도록 하겠습니다.
레퍼런스 카운팅은 Garbage의 Detection, 즉 쓰레기를 찾는 것에 초점이 맞추어진 초기 Algorithm입니다. 각 Object마다 Reference Count를 관리하여 Reference Count가 0이 되면 GC를 수행하죠.
이 그림에서 a와 b는 각각 100, 99의 값을 갖는 Integer Object로 선언되어 있는데요, 각 Integer Object는 Referece Count가 1로 설정되어 있습니다. 이 상태에서 a=b 로 변경하게 되면 a가 참조하고 있던 100의 값을 갖는 Integer Object는 참조가 없어지기 때문에 Reference Count가 0으로 감소하게 되고 GC가 수행됩니다. 이 단순한 방식은 Reference Count가 0이 될 때마다 GC가 발생하기 때문에 시간 작업에도 거의 영향을 주지 않고 즉시 메모리에서 해제된다는 장점이 있으나 각 Object마다 Reference Count를 변경해 주어야 하고 참조를 많이 하고 있는 Object의 Reference Count가 0이 될 경우 연쇄적으로 GC가 발생할 수 있는 문제가 있고 Reference Count의 관리 비용도 큽니다. 또한 대표적인 문제는 Linked List와 같은 순환 참조 구조에서 Memory Leak이 발생할 가능성도 크다는 것입니다. 이러한 이유로, 레퍼런스 카운팅 방식만으로는 모든 경우에 대응하기 어렵습니다. 따라서 다른 가비지 컬렉션 방식들이 필요하게 되었습니다.
Reference Counting Algorithm의 단점을 극복하기 위해 나온 Algorithm으로 대표적인건 마크앤 스윕 방식입니다. 이 방식은 Root에서 시작해 참조의 관계를 추적해나갑니다. 그래서 Tracing Algorithm이라고도 하구요 이름에서 유추할 수 있듯이 Mark Phase와 Sweep Phase로 나뉘게 됩니다. 이 알고리즘은 죽은 객체와 산 객체의 판단을 힙 영역이 아닌 객체로 부터 닿을 수 있는가로 판단하게 됩니다.
다시 말해서, 힙이 아닌 영역에서 참조를 하고 있으면 해당 객체는 산 객체, 힙이 아닌 영역에서 부터 닿을 수 없는 애는 죽은 애로 판단합니다. 이 논힙 영역을 루트라고 부르며 이 루트는 실행중인 쓰레드가 될 수 있고, 전역 변수, 정적변수 로컬변수 등이 있습니다. 마크단계에서는 이 루트에서 시작하여 접근 가능한 모든 객체를 탐색하며 사용 중인 객체에 마크를 표시합니다.
그 다음, '스윕' 단계에서는 마크되지 않은 객체를 해제합니다. 이렇게 하면, 사용 중인 객체가 아닌 객체는 자동으로 해제되어 메모리를 확보할 수 있습니다. 이 마크앤 스윕 알고리즘을 코드로 표현하면 다음과 같습니다.
마크 단계에서 도달할 수 있는 객체들을 넣어두는 곳을 이 파란 worklist라고 하고
루트를 돌면서 도달할 수 있는 객체들에 마크를 하고 worklist에 넣습니다. 이 분홍색 roots의 정보들은 가비지 컬렉터가 가지고 있습니다.
그 다음으로는 워크 리스트를 하나씩 돌며 여기서는 obj에 해당하겠죠
해당 객체가 참조하는 것이 있는지, 해당 객체에 참조 공간이 있는지 확인해주고 있다면,
해당 객체가 참조하는 객체 또한 도달가능한 객체이기 때문에 마크를 해주고 워크리스트에 넣어줍니다.
마지막으로 스윕단계에서는 워크리스트에 해당하지 않는, 닿을 수 없는 객체들을 free를 통해 해제시켜줍니다.
마크 앤 스윕 방식은 레퍼런스 카운팅 방식보다 강력합니다. 레퍼런스 카운팅 방식에서는 순환 참조 문제를 해결하지 못하는데, 마크 앤 스윕 방식은 순환 참조 문제도 해결할 수 있습니다. 하지만 큰 단점으로는 마크 앤 스윕 방식은 메모리 해제를 위해 일시적으로 프로그램 실행을 중지해야 하는 'Stop the World' 상황이 발생할 수 있다는 것입니다.
현재 뮤테이터 스레드가 멈추어야 현재 메모리에서 살아있는 객체를 잘 식별할 수 있기 때문입니다. 이것을 stop the world라고 합니다. 물론 마크앤스윕 방식 말고도 모든 가비지 컬렉션 알고리즘이 stop the world 상황이 발생합니다. stop the world 의 시간을 줄이기 위해 여러가지 알고리즘을 사용한 여러 종류의 가비지 컬렉션들이 개발되었습니다. 이건 추후 더 이야기를 하도록 하겠습니다. 결론적으로 이야기하자면, 마크앤 스윕 방식은 레퍼런스 카운팅의 단점을 보완해주지만 스탑더월드 상태를 야기시킨다 정도로 정리하면 될 것 같습니다.
다음은 세대별 가비지 컬렉션입니다. 메모리 내의 객체를 여러 세대로 분류하여, 각 세대마다 다른 가비지 컬렉션 알고리즘을 적용하는 방식입니다. 이 방식은 빈번한 객체 생성과 동시에 짧은 생명주기를 가진 객체가 많은 경우에 효과적입니다. 위클리 플러터 때 다트의 가비지 컬렉션에 대해 이야기할 때도 해당 알고리즘을 소개했었죠.
일반적으로, 영 제너레이션에서는. 복사 방식을 사용하고, 올드 제너레이션에서는 아까 설명드린 마크앤 스윕 방식을 채택해 사용합니다. 객체가 막 생성된 시점에는 첫 번째 세대(Young Generation)에 속합니다. 이 이유 기억나시나요? 대부분의 객체는 생성된 후 짧은 시간 동안만 유효하며 오랫동안 유효한 객체는 일부에 불과하다는 약한 세대 가설을 전제로 해당 gc알고리즘이 만들어졌기 때문에 일단 생성됐다? 하면 영 제너레이션으로 들어가는 것입니다. Young Generation에서는 가비지 컬렉션이 빈번하게 발생하는 이유도 대부분의 객체가 짧은 생명 주기를 가진다는 전제 때문입니다. 가비지 컬렉션의 주요 기술 중 하나인 "복사(Copying)" 방식은 Young Generation을 두 영역으로 나눈 후, 객체를 한쪽에 할당하고, 그 공간이 가득 차면 살아있는 객체를 판단하고 나머지 영역으로 살아있는 객체를 이동시킵니다. 이렇게 이동하는 것을 반복하게 됩니다.
이 방식의 장점 중 하나는 메모리를 사용한 후, 쓰지 않는 객체들을 모아 한 번에 삭제할 수 있다는 것이고, 객체를 복사하는 과정에서 객체가 사용될 때마다 카운팅하는 레퍼런스 카운팅 방식보다 훨씬 간단하게 처리할 수 있다는 것입니다. 또한, 한 영역이 가득 찰 때마다 전체 객체를 검사하고 이동시키기 때문에 가비지 컬렉터가 언제 메모리를 회수할지 미리 알 수 있으므로, 전체적인 메모리 관리의 성능을 예측 가능합니다.
하지만 이 방식 역시 단점이 존재하는데요. 가장 큰 단점은 메모리 공간이 두 배로 필요하다는 것입니다. 또한, 객체가 복사되는 과정에서 영역이 이동하므로, 포인터가 가리키는 위치를 계속해서 업데이트해주어야 한다는 것입니다. 이는 다소 부하를 유발할 수 있습니다. 또한, 큰 객체가 복사될 때는 메모리 복사 시간이 길어질 수 있습니다.
일부 객체는 짧은 생명 주기를 가지지 않고, 오랫동안 살아남아야 할 필요가 있습니다. 이런 객체는 Old Generation으로 이동하게 됩니다. Old Generation에서는 Young Generation과는 달리, 가비지 컬렉션이 적게 발생합니다. 따라서, Old Generation에서는 'Mark and Sweep'이라는 알고리즘이 사용됩니다.객체가 Mark되면서 사용되고 있는지 여부를 확인하고, 사용되지 않는 객체는 Sweep하여 해제합니다. 해당 알고리즘은 뒤에 언어와 함께 더 자세히 어떻게 언어들에서 쓰이고 있는지 설명드리겠습니다.
우선 자바의 가비지 컬렉터의 동작방식부터 살펴보겠습니다. 자바는 우리가 앞서 공부한 마크앤 스윕과 세대별 가비지 컬렉션을 사용합니다. 마크앤 스윕에서 루트라고 불렀던, 논 힙 영역에 해당하는 애들이 자바에서는 무엇인지 예시 코드를 통해 살펴보도록 하겠습니다.
자바의 메모리 영역에는 스택 영역과 힙 영역이 존재합니다.
스택은 정적으로 할당한 메모리 영역으로 원시 타입의 데이터가 값과 함께 할당이 됩니다. 힙영역에 생성된 오브젝트 타입의 데이터의 참조 값이 할당이 되고,함수가 호출될 때 사용하는 메모리이며 기능 수행이 끝나면 자동으로 반환되는 메모리입니다. 이렇게 말하면 헷갈리니까 이따 예시로 설명해드리겠습니다.
힙은 동적으로 할당한 메모리 영역으로 모든 오브젝트 타입의 데이터가 할당됩니다. 힙 영역의 오브젝트를 가리키는 참조 변수가 스택에 할당이 됩니다. Heap 영역은 Stack 영역과 다르게 보관되는 메모리가 호출이 끝나더라도 삭제되지 않고 유지되기 때문에 가비지 컬렉터는 이 힙 영역을 관리해줍니다.
코드의 메인함수가 실행될 때 원시 타입의 데이터 int는 stack에 할당이 되고, 차례대로 sum까지 stack에 할당이 됩니다. 마지막 String타입은 참조형 데이터 타입을 갖는 객체이기 때문에 참조 변수는 스택에, 해당 데이터는 힙에 쌓이게 됩니다. 메인 함수가 끝나고 스택의 메모리는 반환되고, 힙의 메모리는 지워지지 않습니다. 이 때 이 도달할 수 없는 객체를 unreachable 오브젝트라고 합니다. 이 오브젝트가 가비지 컬렉션의 대상이 됩니다. 이 대상은 gc 알고리즘에 따라 수거되게 되는데, 수거되는 시점에 대해서는 힙 메모리의 구조를 더 자세히 들여다 봐야합니다.
세대별 가비지 컬렉션 알고리즘에 따라 자바의 힙 영역은 young 영역과 old 영역으로 나뉘게 됩니다. young은 더욱 효율적인 gc를 위해 이렇게로 3가지로 나뉘게 되는데요. 에덴이라는 이 맨 첫번째 영역은 에덴 동산을 생각하시면 됩니다. 무언가 새로운 객체가 생성됐을 때 이 에덴 영역에 할당이 됩니다.
에덴이 다 사용이 되면, 가비지 컬렉터가 발생합니다. 이 때 동작하는 가비지 컬렉터는 minor gc라고 합니다. 이곳에선 Copying이 수행되고 그 중 살아남은 객체는 서바이벌 0로 복사됩니다. 이동할 때 에덴의 언리처러블 객체는 해제되고 이 과정이 반복하면서 서바이벌 0가 다 찼을 땐 서바이벌 0에 대해 Copying이 일어나게 됩니다. 여기서 살아남은 객체는 1로 이동하게 되고 이 때 age값이 증가하게 됩니다.
이 과정이 반복되어 에덴영역에서 가비지 컬렉터가 발생했을 땐 또 가비지 컬렉터가 동작하는데 어디로 가냐면, 서바이벌 두 영역중에 객체가 차있는 곳으로 갑니다. 따라서 언제나 서바이벌 0이나 1 둘중 하나는 비어있게됩니다. 에덴에서 바로 서바이벌 1으로 왔기 때문에 age가 차이가 나겠죠. 서바이벌 1이 모두 찼을 때 서바이벌 0으로 가게되고 이 때 Copying이 일어나게 됩니다. age가 또 올라가겠죠.
이 과정을 반복하면서 특정 age값을 넘어서면 그 때 old generation으로 복사됩니다. 이 과정을 promotion이라고 합니다. 이 프로모션 과정이 계속해서 일어나서 old generation이 모두 차면 이때 major 가비지 컬렉터가 수행됩니다. major gc에서는 마크앤 스윕 알고리즘에 따라 수행되기 때문에 시간이 오래걸립니다 사실 이 major gc가 수행되는 방식의 세부 방식으로 수많은 방식 등이 있는데 자바 gc를 파기보단 각 언어별 가비지 컬렉션의 동작 원리가 더 중요한 것 같아서 이 부분은 생략했으니 궁금하신 분들은 java의 major gc에 대한 키워드로 서칭하시면 될 것 같습니다.
다음으로는 파이썬에서는 어떤 방식으로 구현되어있는지 살펴보도록 하겠습니다. 실제로 인스타그램에서 파이썬의 가비지 컬렉션을 비활성화했는데요, 어떤 방식으로 구현되어있는지를 살펴보며 인스타그램이 어떤 방식을 통해 비활성화 했는지를 알아보겠습니다.
파이썬은 참조 카운트(reference counting)와 세대별 가비지 컬렉션(generational garbage collection)을 사용하여 가비지 컬렉션을 수행합니다. 참조 횟수를 증가시키는 방법은 변수에 객체를 할당하는 방법, list에 추가하거나 class instance에서 속성으로 추가하는 등의 data structure에 객체를 추가하는 방법 그리고 객체를 함수의 인수로 전달하는 방법이 있습니다.
Python standard library의 sys 모듈을 사용하면 특정 객체의 reference counts(참조 횟수)를 확인할 수 있는데요.
이 코드를 보고 참조 카운팅이 몇으로 되었는지 맞춰보실 수 있나요? 2입니다. 변수를 생성할 때 1이 올라가고 a를 getrefcount함수로 전달할 때 1이 올라갑니다.
Python은 메모리 관리를 위한 reference counting 외에도 generational garbage collection(세대별 가비지 컬렉션)이라는 방법을 사용하는데요. 우리가 앞에서 봤듯 참조 카운팅의 경우 순환 참조 문제가 발생하기 때문에 파이썬은 이 문제를 세대별 가비지 컬렉션으로 해결합니다. 파이썬의 세대별 가비지 컬렉터는 내부적으로 generation(세대)과 threshold(임계값)로 가비지 컬렉션 주기와 객체를 관리합니다. 세대는 0~2세대로 구분되고 최근 생성된 객체는 0세대(young)에 들어가고 오래된 객체일수록 1세대와 2세대(old)로 이동합니다. 당연히 한 객체는 단 하나의 세대에만 속하게 되구요.
가비지 컬렉터는 0세대일수록 더 자주 가비지 컬렉션을 하도록 설계되어있는데 이 또한 약한 세대 가설을 따르도록 설계가 된것이죠. 만약 0세대에서 수명이 오래되는 객체가 있다면, 해당 객체는 1세대로 이동합니다. 1세대에서는 mark-and-sweep 알고리즘을 사용하여 가비지 컬렉션을 수행합니다. 이러한 방식으로, 0세대에서 순환 참조가 발생하더라도 1세대로 넘어갈 때 메모리 누수를 방지하게됩니다.
각 세대마다 가비지 컬렉터 모듈에는 임계값 개수가 있는데요. 객체 수가 해당 임계값을 초과하면 가비지 콜렉션이 콜렉션 프로세스를 추가 합니다. 해당 프로세스에서 살아남은 객체는 다른 세대로 옮겨지게 되죠. 자바의 age와 비슷한 개념입니다.
파이썬에서는 세대 가비지 컬렉터의 동작을 변경할 수 있다는 점도 특이한데요. garbage collection process를 trigger 하기 위한 임계값 변경, garbage collection process(가비지 컬렉션 프로세스)를 수동으로 trigger 하거나, garbage collection process(가비지 컬렉션 프로세스)를 모두 비활성화할 수 있습니다. gc 모듈을 사용하여 가비지 컬렉션 통계를 확인하거나 가비지 컬렉터의 동작을 변경하는 방법에 대해 살펴보겠습니다.
gc를 import하고 get threshold함수를 사용하면 각각 0,1,2세대에 임계값이 나옵니다. 0세대에서 객체를 할당한 횟수가 700번을 초과하면 가비지 컬렉션이 수행된다는 거죠.
get_count() method를 사용하여 각 세대의 객체 수를 확인할 수도 있습니다. 위 예에서는 youngest generation(가장 어린 세대)에 121개의 객체, 다음 세대에 9개의 객체 oldest generation(가장 오래된 세대)에 2개의 객체가 있다는걸 확인할 수 있네요.
메모리를 확보하기 위해 수행하는 수동 가비지 컬렉션 프로세스는 원하지 않는 결과가 나올 수 있는데요, 위 경고를 무시하고 가비지 컬렉션 프로세스를 관리하려는 경우가 종종 있습니다. 그것이 인스타그램의 경우인데요, Instagram 개발팀은 모든 세대의 임계값을 0으로 설정하여 비활성화했습니다. 이 변경으로 인해 웹 응용 프로그램이 10% 더 효율적으로 실행되는 좋은 결과를 얻었다고 합니다. Instagram의 서버는 자식 프로세스가 마스터와 메모리를 공유하는 마스터 차일드 메커니즘을 사용되어 실행된다고 합니다.
이 과정들은 중요한게 아니고, 결론적으로 인스타그램에서는 자식 프로세스가 생성된 직후에 공유 메모리가 급격히 떨어진다는 사실을 발견하게 됐고 해당 이슈를 추적하다보니 이것은 파이썬 gc 모듈의 gc.collect()가 수행될 때의 문제로 밝혀졌습니다. 이 gc collect는 특정 임계값에 도달했을 때 수행되기 때문에 이 gc.collect의 수행을 임계치를 0으로 하는 것으로 막아 문제를 해결했다고 합니다. 하지만 Insgargm의 웹 어플리케이션 규모는 수백만 명이 사용하니까 어떤 방법을 사용해서든 웹 응용 프로그램의 모든 성능을 한계치로 끌어 올리는 것이 좋겠지만 저희와 같은 대부분의 개발자에게는 가비지 컬렉션과 관련된 Python의 표준 동작이 충분할 것 같습니다.
마지막으로 다트의 가비지 컬렉션입니다. 다트의 가비지 컬렉션에 대해서는 이전에 위클리 플러터에서 다뤘던 부분이지만 한번 더 복습을 한다는 생각으로 들어주시면 감사하겠습니다. 다트에서는 Generational Garbage Collection과 mark and sweep을 사용했었죠
다트에서는 세대가 old와 young으로 나뉘어져 있었고, young generation에서 Young Space Scavenger가 수행됐습니다.
다트의 young space는 둘로 나뉘어져있었습니다. 앞에서 본 자바의 survivor0과 1 처럼 동작하죠.
새로운 객체들은 활성화된 반쪽에 할당되며, 활성화된 반쪽이 가득 차면, 이중 살아있는 객체들은 비활성화된 반쪽으로 복사되었죠.
old generation에서는 마크앤 스윕 알고리즘이 적용이 되었죠.
하지만 우리가 앞에서 살펴본 자바와 파이썬의 마크앤스윕 알고리즘은 스탑더월드 상태가 빈번하게 발생했지만, 다트는 달랐습니다. hook을 통해 엔진에 의해 앱이 현재 실행되지 않고, 작동하지 않는 상태, 유저 상호작용이 없는 때를 감지해서 gc가 성능에 영향을 끼치지 않은채 실행된다는 점이 큰 차이였습니다. 이러한 알고리즘을 점진적 가비지 컬렉션이라고도 부르는데요, 다트는 어떻게 이것이 가능할까요?
Dart VM에서는 일정한 간격으로 샘플링을 수행합니다. Sampling은 프로그램의 실행을 중단시키지 않으며, 성능 분석 및 디버깅에 매우 유용한데요, 이를 통해 프로그램의 성능을 개선하고, 메모리 사용량을 최적화할 수 있습니다.아무튼 이 때, 샘플링된 정보를 바탕으로 앱이 현재 idle 상태인지 판단합니다. 우리도 해당 샘플링된 정보를 확인할 수 있는데 다트 데브툴에서 몰 엑션즈를 클릭해 observatory로 들어가면 다음과 같은 화면이 뜹니다.
여기서 Timeline 탭으로 들어가서 확인할 수 있는데요, 이 탭에서는 애플리케이션이 실행되는 동안 발생한 이벤트들을 시간별로 나열하여 보여줍니다. "Frames" 뷰를 선택한 후, "Idle" 항목을 찾아봐야 합니다. Idle 상태에서는, 화면이 업데이트되는 프레임이 없으므로, Frames 뷰에서는 "Idle" 항목이 화면 전체를 차지하게 됩니다. "Idle" 항목의 색상은 보통 회색으로 나타나며, 이를 통해 애플리케이션이 Idle 상태인지 쉽게 파악할 수 있다고 합니다.
그렇게 앱이 중지 상태로 판단된 경우, Dart VM에서는 Hooking을 수행합니다. Hooking은 Dart VM에서 앱을 실행할 때, 코드의 특정 지점에서 코드 실행을 중지시키고, Dart VM에서 제공하는 훅(Hook) 함수를 실행합니다. Hooking 함수에서는 garbage collection을 수행하게 됩니다. 이 방식을 통해 다트는 블록되지 않고 사용자와 상호작용을 계속 할 수 있습니다.
위의 표에서 확인할 수 있듯이, 자바와 파이썬은 일시 정지(stop-the-world) 방식을 사용하여 gc를 실행하며, gc 실행 시 애플리케이션이 일시적으로 멈춥니다. 반면에 다트는 incremental 방식을 사용하여 gc를 실행하며, gc 실행 시 애플리케이션이 멈추지 않으므로 성능에 영향을 덜 줍니다. 또한, 파이썬은 참조 카운트(reference count) 방식을 사용하여 gc를 실행하는 반면, 자바와 다트는 mark-and-sweep 알고리즘을 사용합니다. 비슷한 알고리즘을 사용해 gc를 수행하지만 각기 어떤 알고리즘과 함께 쓰이는지와 또 가상머신이 어떤걸 제공해주느냐에 따라 굉장히 차이가 많이 난다는 것을 알 수 있습니다.
가비지 컬렉션에 대해 공부하면서 각 언어별로 메모리 구조나 런타임 환경 등에 대해 알 수 있었던 시간이였습니다. 기본적인 알고리즘은 공통적이고 각 언어의 특성에 따라 다양하게 발전된 가비지 컬렉터의 모습도 재미있었습니다. 오늘 발표 들어주셔서 감사합니다.
출처
https://luavis.me/python/dismissing-python-garbage-collection-at-instagram
https://medium.com/dmsfordsm/garbage-collection-in-python-777916fd3189