가비지 컬렉션(Garbage Collection)의 정의와 동작 원리
가비지 컬렉션(Garbage Collection, GC)은 메모리 관리 기법으로, 더 이상 필요하지 않거나 참조되지 않는 객체를 자동으로 메모리에서 제거하는 프로세스입니다.
이를 통해 메모리 공간을 확보하고, 프로그램이 메모리 누수 없이 안정적으로 실행될 수 있도록 돕습니다.
GC는 주로 Java나 C# 같은 언어에서 사용되며, 이러한 언어들은 메모리 관리를 자동으로 처리해주기 때문에 개발자가 메모리 해제에 직접 관여할 필요가 없습니다.
가비지 컬렉션의 필요성
메모리 관리가 중요한 이유는 객체가 더 이상 사용되지 않더라도 메모리에서 해제되지 않으면 메모리 누수가 발생할 수 있기 때문입니다.
메모리 누수는 성능 저하를 일으키고, 심각한 경우 시스템이 메모리를 다 소진해 Out of Memory 에러가 발생할 수 있습니다.
수동으로 메모리 관리를 해야 하는 C나 C++ 언어와 달리, GC를 사용하는 언어에서는 메모리 관리를 자동화하여 이러한 위험을 줄입니다. 이를 통해 프로그래머는 메모리 해제와 같은 복잡한 작업에서 벗어나, 프로그램 로직에 집중할 수 있습니다.
가비지 컬렉션의 기본 개념
GC는 메모리에서 더 이상 참조되지 않는 객체를 가비지(쓰레기)로 간주하고 이를 제거합니다. GC는 주로 힙 메모리(Heap Memory)에서 동작하며, 이 공간에 할당된 객체 중 참조되지 않는 객체들을 수집하여 메모리를 해제합니다.
힙 메모리 구조
힙 메모리는 프로그램에서 동적으로 할당되는 객체들이 저장되는 메모리 공간입니다. 가비지 컬렉션은 이 힙 영역을 관리하며, 필요에 따라 사용되지 않는 객체를 탐색하고 메모리를 회수합니다.
루트 객체(Root Object)
루트 객체는 실행 스택이나 전역 변수와 같이 프로그램에서 직접 접근할 수 있는 객체들입니다. 루트 객체에서 시작해 참조할 수 있는 모든 객체는 활성 객체로 간주됩니다. GC는 루트 객체로부터 참조되지 않는 객체를 가비지로 판정하고 메모리에서 제거합니다.
가비지 컬렉션의 동작 방식
가비지 컬렉션은 다양한 방식으로 동작하며, 가장 기본적인 알고리즘은 Mark and Sweep입니다.
이를 포함해 GC의 동작 방식은 크게 두 단계로 나뉩니다: 참조 가능한 객체를 찾는 단계와 필요 없는 객체를 제거하는 단계.
Mark and Sweep 알고리즘
Mark 단계
가비지 컬렉션은 먼저 프로그램의 루트 객체에서부터 시작해 참조 가능한 모든 객체를 찾아 마킹합니다.
이 마킹 작업은 객체가 여전히 사용 중이라는 것을 나타냅니다. 참조되지 않는 객체들은 마킹되지 않고 가비지로 간주됩니다.
Sweep 단계
마킹되지 않은 객체들은 더 이상 필요 없는 것으로 간주되어 메모리에서 해제됩니다.
이 과정에서 GC는 메모리를 순차적으로 탐색하여 마킹되지 않은 객체를 해제하고, 그 공간을 가용 메모리로 반환합니다.
Stop-the-World
가비지 컬렉션이 실행되는 동안 Stop-the-World (STW) 현상이 발생합니다. 이는 GC가 실행될 때 모든 애플리케이션 스레드의 실행이 일시적으로 중지되는 상태를 말합니다.
STW는 GC가 안전하게 메모리를 관리할 수 있도록 애플리케이션이 메모리에 접근하는 것을 차단합니다. 그러나, 이로 인해 프로그램 응답 시간이 늘어날 수 있기 때문에 STW 시간을 최소화하는 것이 중요합니다.
세대별 가비지 컬렉션(Generational Garbage Collection)
세대별 가비지 컬렉션은 객체의 생명 주기에 따라 메모리 영역을 나누어 관리하는 방식입니다.
대부분의 객체는 생성된 후 짧은 시간 동안만 사용되고, 이후 필요 없어지는 특성을 가지고 있습니다.
이를 기반으로 GC는 메모리를 Young Generation과 Old Generation으로 나누어 관리합니다.
세대별 가비지 컬렉션은 메모리 관리의 구조적 접근법이고, Mark and Sweep은 가비지를 수집하는 방법입니다.
이 두 가지 개념은 동시에 작동하며, 각각의 세대에서 Mark and Sweep과 같은 알고리즘이 적용되어 불필요한 객체를 수집합니다.
Young Generation
Young Generation에는 새로 생성된 객체들이 할당됩니다.
대부분의 객체는 이 영역에서 단기간 사용되다가 가비지 컬렉션에 의해 제거됩니다.
Young Generation에서 수행되는 가비지 컬렉션을 Minor GC라고 부릅니다. Minor GC는 자주 발생하며, 속도도 빠릅니다.
Minor GC를 정확히 이해하려면 Young Generation의 구조부터 알아야 합니다.
Young Generation은 1개의 Eden 영역과 2개의 Survivor 영역(S0, S1)으로 구성되어 있으며, 이 세 개의 영역이 Minor GC의 주요 대상이 됩니다.
Young Generation 구조
- Eden 영역: 새로 생성된 객체가 할당되는 공간입니다. 모든 객체는 처음 생성될 때 Eden 영역에 저장됩니다.
- Survivor 영역(S0, S1): Eden 영역에서 Minor GC가 발생하고 살아남은 객체가 이곳으로 이동합니다. 두 개의 Survivor 영역이 있지만, 항상 한쪽 Survivor 영역만 사용되며, 나머지 하나는 비어 있는 상태로 유지됩니다.
Minor GC 동작 과정
- Eden 영역에 객체 할당: 객체가 처음 생성되면 Eden 영역에 할당됩니다. 이때 객체는 아직 가비지 컬렉션의 대상이 아니며, 사용 가능한 상태입니다.
- Eden 영역이 가득 차면 Minor GC 발생: 객체가 계속해서 생성되면 결국 Eden 영역이 꽉 차게 됩니다. 이 시점에서 Minor GC가 발생합니다. Minor GC는 Eden 영역을 중심으로 불필요한 객체를 제거하고 메모리를 회수하는 작업을 수행합니다.
- Eden 영역의 사용되지 않는 객체 제거: Minor GC가 실행되면 먼저 Eden 영역에서 더 이상 참조되지 않는 객체들을 제거합니다. 이러한 객체들은 메모리에서 완전히 해제되어 가비지(Garbage)로 처리됩니다.
- 생존 객체를 Survivor 영역으로 이동: Eden 영역에서 참조되고 있는 객체는 Survivor 영역으로 이동됩니다. 이때 두 개의 Survivor 영역(S0, S1) 중 하나에만 객체가 저장됩니다.
- Survivor 영역 간의 이동: Minor GC가 반복될 때, 한쪽 Survivor 영역에 있던 객체들이 다른 빈 Survivor 영역으로 이동하게 됩니다. 객체가 여러 번의 Minor GC에서 살아남으면 계속해서 Survivor 영역 간을 이동합니다.
- Old Generation으로 승격(Promotion): 객체가 여러 번의 Minor GC를 거치고도 살아남을 경우, 해당 객체는 Old Generation으로 이동하게 됩니다. 이 과정을 Promotion이라고 하며, 이는 객체가 더 이상 Young Generation에서 관리되지 않고 오래된 객체(Old 객체)로 간주된다는 뜻입니다.
Minor GC가 실행될 때 Young 영역의 객체들 중에서 참조되지 않는 객체들을 정리하게 됩니다. 문제는 Old 영역에 있는 객체들이 Young 영역의 객체를 참조할 수도 있다는 점입니다.
Minor GC는 기본적으로 Young 영역에만 초점을 맞추기 때문에 Old 영역의 모든 객체를 일일이 확인해서 Young 영역의 객체를 참조하는지 확인하는 것은 비효율적입니다. 즉, 불필요한 리소스를 낭비하게 됩니다.
카드 테이블의 역할
이를 해결하기 위해 카드 테이블이라는 구조가 도입되었습니다.
카드 테이블은 Old 영역에 있는 객체가 Young 영역의 객체를 참조할 때 그 정보를 기록해 두는 일종의 "참조 지도" 역할을 합니다.
그래서 Minor GC가 발생할 때는 Old 영역의 모든 객체를 확인할 필요 없이, 카드 테이블만 빠르게 조회해서 어떤 Old 객체가 Young 객체를 참조하는지 파악할 수 있습니다. 이를 통해 GC의 효율성을 높이는 것이 목적입니다.
정리하자면, 카드 테이블은 Old 영역의 객체들이 Young 영역의 객체를 참조하는 경우를 효율적으로 관리해 Minor GC가 더 빠르고 적은 리소스로 실행되도록 도와주는 역할을 합니다.
객체 할당 최적화
객체가 생성될 때마다 Eden 영역에 저장되지만, 이 작업이 자주 일어나기 때문에 어떻게 빠르고 효율적으로 메모리에 객체를 할당할 수 있을까가 중요한 문제입니다.
이 문제를 해결하기 위해 HotSpot JVM에서는 Bump the Pointer와 TLABs라는 기술을 사용합니다.
Bump the Pointer란?
Bump the Pointer는 Eden 영역에 객체를 빠르게 할당하는 방법입니다. Eden 영역에 객체를 할당할 때, 마지막으로 할당된 객체의 주소를 기억해두고, 새롭게 할당할 객체는 그 다음 주소에 바로 할당하는 방식입니다.
이 방법은 효율적입니다. 왜냐하면 매번 유효한 메모리 공간을 탐색할 필요 없이 그저 마지막 주소 다음 위치에 객체를 놓기만 하면 되기 때문이죠. 이렇게 하면 메모리 할당 속도가 매우 빨라집니다. 할당할 객체의 크기가 Eden 영역에 맞는지만 확인하면 바로 할당이 이루어집니다.
싱글 스레드와 멀티 스레드에서의 문제
이 방식은 싱글 스레드 환경에서는 문제가 없습니다. 왜냐하면 하나의 스레드가 계속해서 Eden 영역에 객체를 할당하므로, 다른 작업과 충돌이 발생하지 않기 때문입니다. 하지만 멀티 스레드 환경에서는 문제가 발생할 수 있습니다.
멀티 스레드 환경에서는 여러 스레드가 동시에 Eden 영역에 객체를 할당하려고 하기 때문에 충돌이 발생할 수 있습니다. 이 충돌을 방지하려면 객체를 할당할 때 락(Lock)을 걸어 동기화 작업을 해야 합니다. 하지만 동기화를 하면 성능이 떨어질 수 있습니다. 동기화는 각 스레드가 차례대로 작업해야 하므로, 전체적으로 프로그램이 느려질 수 있기 때문입니다.
TLABs (Thread-Local Allocation Buffers)란?
이 문제를 해결하기 위해 HotSpot JVM은 TLABs(Thread-Local Allocation Buffers)라는 기술을 도입했습니다.
TLABs는 각각의 스레드에게 Eden 영역에서 사용할 수 있는 고유한 메모리 공간을 나눠주는 방식입니다.
이 방식에서는 각 스레드가 자신만의 공간에서 객체를 할당할 수 있습니다.
따라서 스레드 간에 충돌이 발생하지 않고, 동기화 작업이 필요 없게 됩니다. 각 스레드는 자신이 받은 메모리 공간에서 객체를 할당하기 때문에 Bump the Pointer 방식의 빠른 할당 속도를 그대로 유지할 수 있습니다.
TLABs의 동작 방식
- 각각의 스레드가 Eden 영역에서 자신만의 할당 공간을 가지게 됩니다.
- 각 스레드는 할당받은 공간에만 객체를 저장하기 때문에, 다른 스레드와 충돌하지 않고 동기화 작업 없이 빠르게 객체를 할당할 수 있습니다.
- Bump the Pointer 방식으로 객체를 할당하는데, 각 스레드는 자신에게 주어진 공간 내에서만 작업하기 때문에 성능이 유지됩니다.
Minor GC의 특징
- 빠른 처리: Young Generation의 크기가 작기 때문에 Minor GC는 매우 빠르게 처리됩니다.
- 자주 발생: Young Generation에 새로 생성된 객체가 계속해서 할당되므로 Minor GC는 자주 발생합니다. 대부분의 객체는 짧은 생명주기를 가지므로, 빠르게 메모리가 해제됩니다.
- Stop-the-World: Minor GC가 발생할 때 Stop-the-World 이벤트가 일어나 애플리케이션이 일시적으로 멈추지만, 그 시간은 비교적 짧은 편입니다.
Old Generation
Young Generation에서 여러 번 살아남은 객체는 Old Generation으로 이동합니다. Old Generation은 가비지 컬렉션이 덜 자주 발생하며, 이 영역에서의 GC를 **Major GC (Full GC)**라고 합니다. Major GC는 애플리케이션에 더 큰 영향을 줄 수 있으며, Stop-the-World 시간이 길어질 수 있습니다.
Major GC 또는 Full GC는 Old Generation에서 발생하는 가비지 컬렉션입니다.
Old Generation에는 여러 번의 Minor GC를 거쳐 살아남은 객체들이 저장되며, 이 영역의 객체들은 오래된 객체들로 간주됩니다.
Major GC는 Old Generation의 메모리 부족이 발생할 때 실행되며, Old Generation에 있는 객체들을 대상으로 합니다.
Old Generation 구조
- Old Generation은 Minor GC에서 승격된 객체가 저장되는 영역입니다. 이곳에 있는 객체는 생명주기가 긴 객체로, Young Generation보다 덜 자주 GC가 발생합니다.
Major GC 동작 과정
1. Old Generation이 가득 차면 Major GC 발생: Old Generation의 메모리가 가득 차면 Major GC(Full GC)가 발생합니다.
이때 Young Generation도 함께 가비지 컬렉션이 이루어질 수 있습니다. Major GC는 전체 힙 메모리를 대상으로 작업을 진행하기 때문에 Minor GC보다 훨씬 많은 리소스를 소모합니다.
2. Mark 단계: Mark-and-Sweep 알고리즘을 사용하여 루트 객체(root objects)에서 참조되고 있는 객체들을 마킹합니다. 이 작업은 Old Generation뿐만 아니라 Young Generation에 있는 객체들까지 포함됩니다.
3. Sweep 단계: 마킹되지 않은 객체, 즉 참조되지 않는 객체들은 메모리에서 해제됩니다. 이러한 객체는 가비지로 간주되며, 메모리에서 완전히 제거됩니다.
4. Compact 단계(선택적): Mark-and-Sweep 단계에서 메모리 해제가 이루어지면, Old Generation에 메모리 단편화가 발생할 수 있습니다.
Compact 단계는 이 단편화를 해결하기 위해 객체를 한쪽으로 몰아서 정리하는 과정입니다. 이를 통해 메모리 단편화 문제를 해결하고, 새로운 객체를 할당할 수 있는 연속된 공간을 확보할 수 있습니다.
Major GC와 Minor GC의 특징
- 긴 Stop-the-World 시간: Major GC가 발생할 때는 애플리케이션이 중단되는 시간(Stop-the-World)이 상당히 길어질 수 있습니다. 이는 Major GC가 Old Generation 전체를 대상으로 하고, Young Generation까지 포함하는 경우가 있기 때문입니다.
- 메모리 단편화 문제: Major GC는 Sweep 단계에서 참조되지 않는 객체를 제거하면서 메모리 단편화가 발생할 수 있습니다. 이 경우 Compact 단계를 통해 메모리 단편화를 해소해야 합니다.
- 성능에 큰 영향: Full GC는 전체 애플리케이션 성능에 큰 영향을 줄 수 있습니다. 특히, 대규모 메모리를 사용하는 애플리케이션에서는 Full GC가 성능 병목을 일으킬 수 있습니다.
가비지 컬렉션의 장점과 단점
장점
- 메모리 관리 자동화: 개발자가 명시적으로 메모리를 해제하지 않아도 되므로, 메모리 관리에 대한 부담을 덜어줍니다.
- 안전성: 메모리 누수와 같은 문제가 줄어들고, 시스템의 안정성이 높아집니다.
- 생산성 향상: 메모리 해제를 신경 쓰지 않아도 되므로 개발자가 비즈니스 로직에 더 집중할 수 있습니다.
단점
- Stop-the-World: 가비지 컬렉션이 발생할 때 애플리케이션의 모든 작업이 중지되기 때문에 실시간 애플리케이션에서는 성능 저하를 일으킬 수 있습니다.
- 성능 저하: GC가 메모리를 정리하는 동안 CPU 리소스를 많이 사용하여 애플리케이션의 성능에 영향을 미칠 수 있습니다.
- 메모리 단편화: Sweep 단계에서 메모리 단편화가 발생할 수 있으며, 메모리 할당 시 성능에 영향을 줄 수 있습니다.
결론
가비지 컬렉션은 메모리 관리를 자동화하여 개발자의 생산성을 높이고, 메모리 누수나 관리 오류를 방지합니다.
그러나 Stop-the-World와 같은 중단 시간이 발생할 수 있어, 이를 최소화하기 위한 다양한 최적화 기술과 GC 알고리즘이 사용됩니다. GC의 기본 원리와 동작 방식을 이해하면, 시스템 성능을 최적화하고, 메모리 관련 문제를 효과적으로 해결할 수 있습니다.
'개발' 카테고리의 다른 글
[배포] github actions, EC2, nginx를 통한 무중단 배포 (3) | 2024.10.09 |
---|---|
[spring] spring security 완전 정복 (4) | 2024.10.09 |
[spring mvc] 서블릿 컨테이너 너는 누구냐 (3) | 2024.10.06 |
[spring mvc] DispatcherServlet 핵심 정리 (0) | 2024.10.06 |
[spring boot + JPA] 동시성 문제 (2) | 2024.10.06 |