01-06. Garbage Collection

Q. Garbage Collection에 대해 아시는데로 말씀해주세요.

프로그래밍에서 객체나 변수를 생성하면, 이는 마치 책상 위에 물건을 올려두는 것처럼 메모리 공간을 차지하게 됩니다. 그런데 더 이상 사용되지 않거나 참조되지 않는 객체가 남아 있다면, 마치 사용하지 않는 물건이 책상 위를 계속 차지하고 있는 것처럼 새로운 작업에 필요한 공간을 확보할 수 없어 메모리가 비효율적으로 낭비됩니다. 이러한 메모리 누수를 방지하기 위해 Garbage Collection(가비지 컬렉션) 이 필요합니다.

한마디로 Garbage Collection프로그래밍에서 더 이상 사용되지 않는 메모리를 자동으로 회수하는 메모리 관리 기법입니다.

이를 구현하는 방식에는 몇 가지가 있는데요.

가장 단순한 방식은 참조 카운팅(Reference Counting) 으로, 객체가 몇 개의 참조를 받고 있는지 숫자로 관리하다가 참조 수가 0이 되면 그 즉시 메모리를 회수하는 방식입니다. 하지만 이 방법은 순환 참조를 처리하지 못한다는 단점이 있습니다.

이를 보완한 방식이 마크 앤 스위프(Mark and Sweep) 입니다. 이 방법은 먼저 사용 중인 객체를 찾아 ‘마크’한 뒤, 마크되지 않은 객체들을 한꺼번에 회수하는 구조라 순환 참조도 문제되지 않습니다.

그리고 또 다른 방식으로는 복사 수집(Copying Collection) 이 있는데요, 살아 있는 객체들만 골라 새로운 메모리 영역으로 복사하고, 기존 메모리 영역은 통째로 비워버리는 효율적인 방식입니다. 보통 JavaYoung Generation에서 이 방식이 활용됩니다.

이러한 Garbage Collection을 통해 메모리 누수를 방지하고 메모리 관리를 자동화하여 개발자의 부담을 줄여줍니다.

Q. 순환 참조가 있는 객체들도 마크 앤 스위프에서는 수거할 수 있는 이유는 뭔가요?

마크 앤 스위프 방식이 순환 참조를 수거할 수 있는 이유는, 참조가 존재하느냐 보다 루트에서 도달 가능한 객체냐를 기준으로 판단하기 때문입니다.

일단 마크 앤 스위프 방식은 두 단계로 이루어지는데요.

먼저 마크(Mark) 단계에서는 GC 루트(Root)라고 불리는 시작점에서부터 객체를 따라가며 접근 가능한 객체들에 마크를 남겨요. 이 마크는 "이 객체는 아직 살아있다" 는 표시입니다.

그다음 스위프(Sweep) 단계에서는 마크되지 않은 객체들 즉, 루트에서 도달할 수 없는 객체들을 전부 메모리에서 회수하게 됩니다.

그럼 A 객체가 B를 참조하고, B도 다시 A를 참조하고 있는 경우인 순환 참조에서 이 두 객체가 서로를 계속 가리키고 있어서 참조 수는 0이 아니지만, 루트에서 이 두 객체로 가는 길이 없다면, 결국 도달 불가능한 객체가 되는 거죠.

마크 앤 스위프는 바로 이걸 회수할 수 있습니다.

그래서 참조 카운팅처럼 ‘누가 나를 가리키고 있냐’만 보는 방식은 순환 참조를 못 처리하지만, 마크 앤 스위프는 루트에서의 접근 가능성을 기준으로 판단하기 때문에 이런 순환 구조도 깔끔하게 수거할 수 있는 겁니다.

Q. 그렇다면 GC는 자바에서는 어떻게 동작하나요?

자바(Java)에서는 Garbage Collection이 자동으로 동작합니다. 개발자가 따로 객체를 해제할 필요 없이, 자바가 알아서 필요 없는 개체를 찾아서 메모리에서 치워주는 거죠.

예를 들어, 우리가 만든 데이터가 어떤 객체를 참조하고 있다가, null로 바뀌면 이제 더 이상 그 객체를 참조하는 변수가 없잖아요? 그러면 자바는 "이거 아무도 안쓰네?" 하고 이걸 가비지 컬렉션 대상으로 인식하고, 적당한 시점에 메모리에서 회수합니다.

이처럼 자바 GC는 내부적으로 두 가지 큰 단계로 이루어져 있습니다. 먼저 어떤 객체들이 아직 살아있는지를 확인하고, 그 다음에 죽은 객체들만 골라서 메모리에서 치우는 방식이죠. 이게 앞에서 말한 마크 앤 스위프 방식입니다.

근데 자바는 이런 메모리. 즉, 힙 메모리를 크게 두 영역으로 나눠서 관리합니다. 하나는 Young Generation이고, 하나는 Old Generation이에요.

새로 생성된 객체는 처음엔 Young 영역에 들어갑니다. 이 영역은 크기가 작고, 대부분 객체가 금방 사라지기 때문에 자주 빠르게 GC가 일어납니다. 이걸 Minor GC라고 불리는데 Minor GC는 멈추는 시간이 짧고, 성능에도 부담이 적습니다.

그런데 어떤 객체가 계속 살아남으면, 자바는 "얘는 오래 쓸 것 같네?"라고 판단하고 그 객체를 Old Generation으로 옮겨요. 이쪽은 메모리 공간도 더 크고, 상대적으로 GC가 덜 자주 발생하는데, 한 번 발생하면 Mark and Sweep 같은 무거운 작업이 들어갑니다. 이 GC를 Major GC 또는 Full GC라고 부릅니다. 이때는 Stop-the-World 현상, 그러니까 프로그램 실행이 잠깐 멈추는 현상이 발생할 수 있습니다.

GC가 이처럼 프로그램을 멈출 수도 있기 때문에, 자바는 다양한 GC 알고리즘 예를 들면 G1 GC, ZGC 같은 것들을 개발해서 성능과 정지 시간을 최적화하고 있습니다.

Q. 그럼 자바의 다양한 GC 방식은 어떤 게 있나요?

첫 번째는 Serial GC 입니다. 이건 아주 단순한 구조의 GC로, GC를 처리하는 쓰레드가 하나뿐입니다. 그래서 CPU가 1개인 환경 예를 들면 임베디드 시스템이나 테스트 환경에서는 괜찮은데, GC가 도는 동안 모든 스레드가 멈추는 Stop-the-World 시간이 가장 깁니다. 요즘 서버 환경에서는 거의 안 쓰이고, 테스트용이나 학습용으로 쓰이는 경우가 많죠.

두 번째는 Parallel GC 입니다. 이건 Java 8까지의 기본 GC 였습니다. Serial GC처럼 전체 힙을 청소하긴 하지만, Young 영역의 GC를 멀티 쓰레드로 수행합니다. 그래서 Stop-the-World 시간이 줄어들고, 처리 속도도 훨씬 좋아졌어요. 단순하고 안정적이라서 지금도 많이 사용됩니다.

세 번째는 Parallel Old GC 입니다. 이건 Parallel GC의 확장판인데요, Old Generation 영역까지 멀티 쓰레드로 GC를 처리하도록 개선된 버전입니다. 청소 방식도 좀 달라져서, 기존 Mark and Sweep 대신에Mark-Summary-Compact라는 방식이 들어갔어요. 간단히 말하면, 살아있는 객체 위치를 정리해서 연속된 공간으로 만들고,조각난 메모리를 정리해주는 방식 입니다.

네 번째는 G1 GC(Garbage First) 입니다. Java 9부터는 기본 GC가 G1으로 바뀌었는데, 이건 기존처럼 Young/Old를 딱 고정된 공간으로 나누는 게 아니라,Region이라는 작은 조각들로 힙을 잘게 쪼개요. 그중에서 Garbage가 많은 Region부터 먼저 청소해서 효율을 높입니다. GC 빈도를 줄이고, 정지 시간을 예측 가능한 수준으로 관리할 수 있어서지연 시간(Latency)을 중요시하는 서버 환경에서 많이 씁니다.

그리고 이 G1 GC의 Region 개념을 기반으로 더 발전된 GC들도 계속 나오고 있습니다. 대표적인 게 Shenandoah GCZGC인데, 이 둘은 공통적으로 정지 시간을 수 밀리초 단위로 줄이는 걸 목표로 만들어졌다고 합니다.

Last updated