GC(Garbage Collection)이란?
직역하자면 ‘쓰레기 수집’인데, Computer Science 분야에서는 프로그램이 동적으로 할당했던 메모리 영역 중에서 필요 없게 된 영역을 해제하는 ‘메모리 관리의 한 형태’를 말한다. 필요 없게 된 영역이란 의미는 어떤 변수도 가리키지 않게 된 영역을 의미한다.
GC가 없다면 개발자는 직접 메모리를 해제해야 하기에, 각종 오류나 잠재적인 버그 발생의 위험이 있다.
GC가 필요한 이유
GC를 사용하지 않을 때 발생할 수 있는 문제점을 알아보자.
- 메모리 누수(Memory Leak): 사용되지 않는 메모리가 해제되지 않아, 점점 사용 가능한 메모리가 줄어들 수 있다.
- 더글라스 올린(Dangling Pointer): 이미 해제된 메모리 주소를 참조하는 포인터가 남아있는 문제로, 해당 포인터로 동작을 시도할 경우 예측할 수 없는 무작위의 결과가 발생할 수 있다.
- 이중 해제(Double Free): 이미 해제된 메모리의 포인터를 무효화하지 않고, 다시 해제하려 할 때 발생하는 문제로, 메모리 관리 시스템에 충돌을 일으켜 전체 시스템이 동작을 멈출 수도 있다.
GC는 위와 같은 문제들을 자동으로 관리하여 개발자가 메모리 관리에 신경쓰지 않고 비즈니스 로직에 집중할 수 있게 해준다. 하지만 GC를 사용함으로써 발생하는 문제도 분명히 존재한다.
- 성능 오버헤드: GC는 메모리를 자동으로 관리하기 때문에, 프로그램 실행 중에 추가적인 연산 오버헤드가 발생할 수 있다. 이로 인해 시스템의 응답 시간 지연이나 전반적인 성능 저하가 발생할 수 있다.
- Stop-The-World 현상: GC는 메모리를 관리하기 위해 사용하지 않는 객체를 식별하고 해제하는 과정을 수행하는데, 이 과정은 모든 스레드가 안전한 상태에 있을 때만 수행이 가능하다. 따라서 GC는 시스템의 모든 스레드를 일시적으로 중단시키며, 이로 인해 시스템의 응답성이 저하된다.
이러한 문제점 때문에 실시간 애플리케이션에서는 부적합할 수 있으며, 적합한 알고리즘 선택, GC 튜닝 등이 필요하다.
GC의 동작 흐름
- 객체 할당(Allocation): 프로그램이 새로운 객체를 생성할 때 메모리 공간이 할당된다.
- 루트 집합(Root Set) 정의: GC는 먼저 ‘루트 집합’을 정의한다. 루트 집합은 애플리케이션이 직접 참조하는 모든 객체로, 일반적으로 전역 변수, 스택 변수, CPU 레지스터 등이 포함된다. JVM으로 생각해 보자면 활성 프레임의 지역 변수, 매개변수, static 변수 등이 있다.
- 루트 집합은 GC가 도달 가능성을 평가하는 시작점이 된다.
- 객체 식별(Marking): GC는 루트 집합에서 시작하여 모든 도달 가능한 객체를 확인한다. 이 과정을 마킹 단계라고 하며, 참조를 통해 접근할 수 있는 모든 객체를 '마크'한다.
- 마크된 객체는 현재 사용 중이거나 다른 객체에 의해 참조되고 있음을 나타낸다.
- 객체 해제(Sweeping): 마크되지 않은 객체는 더 이상 사용되지 않는 것으로 간주되어 메모리에서 해제된다. 이 과정에서 다양한 GC 알고리즘이 사용될 수 있다.
- 메모리 압축(Compaction) (선택적): 메모리 단편화를 줄이기 위해 일부 GC 알고리즘은 메모리를 압축하여 남아 있는 객체를 한쪽으로 몰아놓는다. 이를 통해 새로운 객체 할당을 위한 연속된 메모리 블록을 제공할 수 있다.
메모리 단편화(memory fragmentation)란?
사용 가능한 메모리가 충분히 있지만, 이 메모리가 작고 불연속적인 조각으로 나뉘어 큰 연속 메모리 영역으로 할당할 수 없는 상태. 외부 단편화와 내부 단편화가 있다.
다양한 프로그래밍 언어에서의 GC
C/C++을 제외한 주요 프로그래밍 언어들(자바, 파이썬, 닷넷/C# 등)은 대부분 가비지 컬렉션을 구현하고 있다. 아래에서 GC 알고리즘과 해당 알고리즘을 사용하는 언어를 간단하게 알아보자.
- 마크 앤 스윕(Mark-and-Sweep): 마킹 단계에서 도달 가능한 객체를 식별하고, 스윕 단계에서 마크되지 않은 객체를 해제한다. Java의 초기 GC 알고리즘 등에 사용되었다.
- 마크 앤 컴팩트(Mark-and-Compact): 마크 앤 스윕 방식에서 메모리 압축 단계를 추가했다. Java의 G1 GC에서 사용된다.
- 복사형(Copying): 메모리를 두 개의 영역으로 나누고, 활성 객체를 한 영역에서 다른 영여그올 복사한다. 복사 후 남은 메모리는 전부 해제되어 메모리 단편화가 발생하지 않는다. Java의 Young Generation에서 사용된다.
- 생성 세대(Generational GC): 객체를 생성 주기에 따라 Young Generation과 Old Generation으로 나누어 관리한다. Young Generation에서는 짧은 수명의 객체를 빠르게 수집하고, 오래된 객체는 Old Generation으로 이동한다. HotSpot JVM의 기본 GC 방식이다.
- 참조 카운팅(Reference Counting): 객체가 참조될 때마다 참조 수를 증가시키고, 참조가 사라질 때마다 감소시킨다. 참조 카운트가 0이 되면 객체를 해제하는데, 정지 시간이 거의 없고 사용되지 않을 경우 즉시 회수할 수 있지만 순환 참조 문제를 해결하지 못할 수 있다. Python에서 주로 사용된다.
Java와 GC
Java의 실행 환경이라고 볼 수 있는 JVM(Java Virtual Machine) 안에 가비지 컬렉션이 존재한다.
JVM의 Runtime Data Area(메모리 영역)은 5가지로 나뉘는데, GC는 Heap 메모리를 관리한다.
Heap 메모리는 Young Generation과 Old Generation으로 나뉘어져, 객체의 수명이 짧다는 점을 전제하여 생성 세대 방식으로 관리된다.
Young Generation
- Eden Space와 Survivor Space(S0, S1)로 구성된다.
- Eden: new를 통해 새로 생성된 객체가 위치한다.
- Survivor 0/1: 최소 1번의 GC를 살아남은 객체가 존재한다.
- age: Survivor 영역에서 객체가 살아남은 횟수를 의미한다.
- Young Generation에서 발생하는 GC를 Minor GC라고 칭하며, 대부분 빠르고 빈번하게 발생하며 이 단계에서 수명이 끝난다.
- 동작 순서: 새로 생성된 객체는 Eden 영역에 위치한다. Eden영역이 가득차면 GC가 발생하고, Survivor 영역 중 1개로 이동한다. Eden 영역에서 또다시 GC가 발생하면 기존에 객체가 존재하는 Survivor 영역으로 이동하게 된다. 이동한 Survivor 영역이 가득 차면 GC를 수행하고 다른 Survivor 공간으로 이동한다. 이렇게 반복되는 작업을 ‘Aging’이라고 칭하며, Old 영역으로 넘어가는 기준이 된다.
Old Generation
- Young Generation에서 해제되지 않은 객체가 이동하는 영역이다.
- 객체가 장기적으로 사용된다고 판단되면 이곳으로 이동된다. 이때, age의 임계값이 그 기준이며 이동되는 행위를 ‘Promotion’이라고 한다.
- Old Generation에서 발생하는 GC를 Major GC(Full GC)라고 칭하며, Young에 비해 큰 공간을 갖고 있으므로, GC에 상대적으로 시간이 오래 걸린다.
주요 GC 알고리즘
- Serial GC: 단일 스레드로 GC 작업을 수행한다. 작은 애플리케이션에 적합하고, 멀티코어 환경에서는 비효율적이다.
- Parallel GC: Java8의 default GC이다. Serial GC와 기본적인 동작은 같지만, Young 영역의 Minor GC를 멀티 스레드로 수행한다.(Old는 여전히 싱글 스레드) GC 스레드는 기본적으로 CPU의 갯수만큼 할당된다.
- G1 (Garbage-First) GC: Java9 이상 버전의 default GC이다. 힙을 여러 개의 고정 크기 영역(Region)으로 나누고, 각 영역을 독립적으로 수집한다. 가장 먼저 수집할 필요가 있는 영역부터 수집하는 방식으로, 예측 가능한 성능을 목표로 함
- ZGC (Z Garbage Collector): 매우 낮은 중단 시간을 목표로 하며, 아웃-오브-프로세스 스레드를 사용하여 힙 전체를 관리한다. 연속적인 메모리 압축을 수행한다.
- Shenandoah GC: G1 GC와 유사하지만, 메모리 압축을 병렬로 수행하여 더 짧은 중단 시간을 제공한다.
참고
'Java > 일반' 카테고리의 다른 글
Java의 상속(Inheritance)과 컴포지션(Composition) (0) | 2024.09.26 |
---|---|
Java에서 상수(constant)와 리터럴(literal)이란? (🎈static final과 final의 차이) (0) | 2024.09.24 |
오버라이딩(Overriding) vs 오버로딩(Overloading) (✅오버라이딩의 반환타입, 오버로딩의 형변환) (0) | 2024.08.20 |
Java 제네릭(Generics)를 알아보자! (와일드카드 ?와 T의 차이, 제네릭 메서드 읽어보기) (0) | 2024.08.15 |
JVM(Java Virtual Machine)은 어떻게 동작할까? (0) | 2024.08.14 |
댓글