Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

가비지 컬렉션 소개 (2부): 루비의 GC는 어떻게 동작하는가?

지난 Ruby Magic 시리즈에서는 가비지 컬렉션(GC)이 왜 필요한지, 그리고 전반적으로 어떻게 동작하는지 살펴보았습니다. 이번 글에서는 한 단계 더 나아가, 루비(Ruby) 내부에서 GC가 실제로 어떻게 구현되어 있는지 자세히 알아보겠습니다.

다양한 루비 구현체

루비에는 여러 가지 구현체가 존재합니다. 대표적으로 MRI(Matz's Ruby Interpreter), Rubinius, JRuby 세 가지가 널리 사용됩니다. 각 구현체마다 사용하는 GC 방식이 다른데, 이 글에서는 가장 많은 루비 개발자가 사용하는 MRI를 기준으로 설명하겠습니다.

루비의 힙(Heap)

컴퓨터의 메모리는 크게 스택(Stack)과 힙(Heap) 두 가지로 나뉩니다. 스택은 매우 빠르며 함수 호출 컨텍스트에 국한됩니다. 즉, 스택에 선언된 변수는 해당 함수의 실행이 끝나는 순간 자동으로 해제됩니다. 하지만 스택은 크기가 매우 제한적이기 때문에 이미지나 파일 데이터처럼 용량이 큰 객체를 저장할 수 없습니다.

이러한 특성 때문에 스택은 루비 객체를 저장하기에 적합하지 않습니다. 루비 객체는 하나의 메서드 호출보다 오래 유지되는 경우가 많고, 객체가 스택 크기를 초과할지 여부도 사전에 예측하기 거의 불가능합니다.

그래서 루비는 다른 종류의 메모리인 힙을 사용합니다. 프로그램은 힙에서 메모리를 할당받아 사용하고, 사용이 끝나면 직접 정리할 책임을 집니다. 루비는 루비 객체를 저장하기 위해 하나의 메모리 덩어리(slab)를 통째로 할당해 사용하는데, 이를 '루비의 힙'이라고 부릅니다.

스택과 힙 요약
스택 메모리매우 빠름
함수 호출이 끝나면 사용한 메모리가 자동 해제됨
크기가 매우 제한적
힙 메모리스택보다 약간 느림
자동 정리 기능 없음
컴퓨터의 가용 메모리만큼 사용 가능

따라서 루비 객체는 항상 루비의 힙에 할당됩니다. 객체가 40바이트보다 작으면 그 내용을 객체 안에 직접 담을 수 있고, 그보다 크면 객체는 루비 힙의 별도 메모리 영역을 가리키게 됩니다. 즉, 하나의 루비 객체가 메모리상 서로 다른 두 위치에 저장되는 경우가 흔합니다. 루비의 힙이 가득 차면 새로운 힙을 만들어 새 객체들을 저장합니다.

마크 앤 스윕(Mark and Sweep)

MRI는 '마크 앤 스윕'이라 불리는 GC 알고리즘을 사용합니다. 먼저 마크(mark) 단계에서 가비지 컬렉터는 현재 존재하는 모든 객체를 훑으며, 정리해도 된다고 판단되는 객체에 마크 플래그를 설정합니다.

마크 단계에서는 코드 실행이 일시 중단됩니다. 가비지 컬렉터가 객체들 사이의 모든 관계를 정확히 파악해야 하기 때문입니다. 마크 과정 도중에 프로그램이 계속 실행되면 객체 상태가 변할 수 있고, 컬렉터는 객체의 현재 상태를 확신할 수 없게 됩니다.

그다음 스윕(sweep) 단계가 시작됩니다. 루비 1.9 이상에서는 이 단계가 백그라운드에서 실행됩니다. 가비지 컬렉터는 마크 단계에서 표시된 객체들을 조용히 해제하며, 해당 메모리는 스윕이 완료된 후에야 다시 사용할 수 있게 됩니다.

마크 단계는 코드 실행을 멈추기 때문에 프로덕션 환경에서 문제가 발생할 수 있는 지점입니다. 반면 스윕 단계는 상대적으로 부담이 적습니다.

마크 앤 스윕 요약
마크 단계존재하는 객체를 스캔
정리 가능한 객체에 마크 플래그 설정
코드 실행 중단
스윕 단계백그라운드에서 실행
마크된 객체 정리
스윕 완료 후 메모리 재사용 가능

메이저 GC와 마이너 GC

루비 2.1부터 가비지 컬렉터는 메이저(major)와 마이너(minor) 실행을 구분합니다. 컬렉터는 어떤 객체가 새로 생성되었는지 추적하고, 몇 번의 GC 주기를 거쳐 살아남은 객체는 '오래된(old)' 객체로 분류합니다. 마이너 실행 시에는 이런 오래된 객체들을 무시합니다. 덕분에 가비지 컬렉터가 방금 할당된 객체만 검사하면 되므로 마이너 실행은 훨씬 가볍습니다.

이 방식이 유용한 이유는, 메모리 중 상당 부분은 사실 절대 정리하면 안 되는 경우가 많기 때문입니다. 예를 들어 Rails를 부팅하면 Rails 프레임워크 전체가 메모리에 로드되며, 이는 애플리케이션이 살아있는 동안 계속 유지됩니다. 이런 객체들을 매번 검사하는 것은 명백한 자원 낭비입니다.

메이저/마이너 실행 비교
메이저 실행실행 빈도가 낮음
실행 부담이 큼
마이너 실행오래된 객체 무시
실행 빈도가 높음
실행 부담이 적음

더 깊이 알고 싶은 분들을 위해

이 주제에 관해서는 Aman Gupta의 훌륭한 블로그에서 많은 것을 배웠습니다. 더 깊은 내용을 원하신다면 꼭 참고해 보시기 바랍니다.

다음 편 예고: 실전 가비지 컬렉션 튜닝

가비지 컬렉터의 동작을 최적화하기 위해 측정할 수 있는 지표와 조정할 수 있는 설정 옵션이 여러 가지 있습니다. GC 시리즈의 다음 편에서는 이러한 지표와 설정 파라미터를 자세히 다룰 예정입니다.