olrlobt
[JAVA] Garbage Collection(GC , 가비지 컬렉션)과 Garbage Collector의 종류 본문
[JAVA] Garbage Collection(GC , 가비지 컬렉션)과 Garbage Collector의 종류
olrlobt 2023. 10. 19. 17:08Garbage Collection (GC , 가비지 컬렉션)
Garbage Collection은 JAVA의 메모리 관리 방법 중 하나이며, JVM에 내장된 가바지 컬렉터로 메모리를 관리하는 방법이다.
말 그대로 쓰레기, 개발자가 동적으로 할당한 메모리 영역 중 더 이상 사용하지 않는 객체나 데이터를 찾아내어 메모리를 회수하는 과정을 의미한다. 가비지 컬렉션은 자동 메모리 관리 방식으로, 개발자가 객체 생성을 자유롭게 하고 직접 메모리를 해제할 필요 없게 한다.
Garbage Collector (가비지 컬렉터)
Garbage Collector는 Garbage Collection을 수행하는 구체적인 시스템이나 알고리즘을 의미한다. JVM에 내장된 가비지 컬렉터는 Serial GC, Parallel GC, G1 GC 등 여러 가지가 있으며, 각 컬렉터는 서로 다른 전략을 사용하여 GC(가비지 컬렉션)을 수행한다.
또한, Garbage Collector의 역할은 메모리 할당, 사용 중인 메모리 인식, 사용하지 않는 메모리 인식이 있다.
많은 개발자와 문서에서 GC라는 단어는 가비지 컬렉션(Garbage Collection)을 의미하지만, 위와 같이 맥락에 따라 가비지 컬렉터(Garbage Collector)를 의미하기도 한다.
GC 동작원리
Java의 메모리 영역은 Heap 메모리와 Non-heap 메모리로 나뉜다.
이 중, GC가 발생하는 영역은 Heap 메모리이다.
Java Heap 구조
Java Heap 메모리는 인스턴스, 배열 쌓이는 곳이다. Heap 메모리는 공유메모리라고도 불리며, 여러 스레드에서 공유하는 데이터들이 저장되는 메모리이다.
Java Heap은 객체의 생존 기간과 GC의 최적화를 위해, 크게 두 부분 Young Generation과 Old Generation으로 나누어 설계되었다. 그리고 Young Generation 영역은 Eden 영역과 두 개의 Survivor 영역으로 나누어지므로, Java Heap의 구조는 크게 4개의 영역으로 나누어 볼 수 있다.
Young Generation
Young Generation은 새로 생성된 객체들이 위치하는 공간이다. 대부분의 객체는 금방 사용되지 않게 되므로, 이 영역에서 빠르게 수집된다.
Eden Space:
객체들이 처음 생성될 때 할당되는 영역이다. Eden 영역의 데이터가 꽉 차면, 이 영역에 있는 객체는 어딘가로 옮겨지거나 삭제되어야 한다.
Survivor Space:
이 영역은 우선순위 없는 두 부분(S01과 S02)으로 나뉘며, 두 개의 영역 중 한 영역은 반드시 비어 있어야 한다. Eden에서 GC 이후 살아남은 객체들이 Survivor의 빈 영역으로 이동한다.
Survivor의 한 영역이 꽉 차면 GC가 되면서 Eden 영역에 있던 객체들과 함께, 또 다른 Survivor 영역으로 이동하게 된다.
Old Generation
Old Generation 영역에 있는 객체들은 일반적으로 애플리케이션 수명주기 동안 계속 사용되는 객체들이다.
Young Generation에서 Survivor의 GC 사이클을 여러 번 살아남은 객체들이 이동하는 영역이고, 살아남는 횟수는 JVM의 설정 값에 따라 달라진다. 기본적으로는 대부분 15회 정도라고 생각하면 된다.
또한, Old Generation의 크기가 Young Generation의 크기보다 크기 때문에, 객체가 아주 큰 경우에는 Young Generation에서 Survivor을 거치지 않고 바로 Old Generation으로 넘어갈 수 있다.
Minor GC와 Major GC
Minor GC:
Young Generation 내에서 가비지 컬렉션을 수행할 때 발생하는 GC를 "Minor GC"라고 한다. Minor GC는 Young Generation 내에서만 발생하며, 비교적 빈번하게 발생하지만 영역이 작기 때문에 대체로 빠르게 처리된다.
Major GC (또는 Full GC):
Old Generation에서 가비지 컬렉션을 수행할 때 발생하는 GC를 "Major GC" 또는 "Full GC"라고 한다. Major GC는 Old Generation 뿐만 아니라 전체 Heap을 대상으로 하기도 하며, Minor GC에 비해 덜 빈번하지만, 실행 시간이 훨씬 길다는 특징이 있다.
Garbage Collector의 종류
JDK 7 이상에서 지원하는 GC 방식에는 다섯 가지가 있다.
- Serial Collector
- Parallel Collector
- Parallel Compacting Collector
- CMS Collector
- G1 Collector
각각의 Garbage Collector는 약간의 차이가 있지만, Stop-The-World와 Mark and Sweep 알고리즘을 토대로 GC를 진행한다.
1. Stop-The-World
우리가 GC를 신경 써야 하는 이유는, GC가 진행되는 동안 애플리케이션의 다른 동작들은 모두 멈추기 때문이다.
이렇게 JVM에서 GC를 실행하는 스레드를 제외한 나머지 스레드의 작업을 모두 멈추는 현상을 Stop-The-World라고 부른다.
Stop-The-World가 발생하는 이유
GC를 수행하는 과정에서 사용하지 않는 객체를 식별하고, 회수하는 과정이 진행된다. 이 과정에서 애플리케이션의 스레드가 객체에 접근하는 것을 막기 위해 Stop-The-World가 발생한다.
또한, 일부 GC 알고리즘 수행과정에서 Compact 단계를 실행할 때 살아있는 메모리를 이동시켜 메모리 공간을 효율적으로 쓰게 해야 하는데, 이 과정(메모리 조각화를 해결하는 과정)에서 객체를 새로운 주소로 이동시키고 다시 주소를 참조하는 과정이 필요하게 된다. 이때 애플리케이션의 스레드가 해당 객체에 접근하는 것을 막기 위해 Stop-The-World가 발생하게 된다.
메모리 조각화 :
가비지 컬렉션을 통해 불필요한 객체들이 해제되면서 메모리의 여러 위치에 여유 공간이 생기게 되는데, 이러한 여유 공간들이 연속되지 않고 여기저기 분산되어 있을 때 발생하는 현상.
공간이 많아도, 분산으로 인해, 연속적인 공간이 부족하여 메모리 할당을 실패하는 문제가 발생할 수 있다.
2. Mark and Sweep
Mark (표시):
- 살아있는 객체를 식별하고 표시하는 단계
- Root Set에서 시작하여 접근 가능한 모든 객체를 표시한다.
Root Set :
GC root의 집합
GC root :
GC과정에서 생존하는 객체를 판별하기 위한 시작점.
예를 들어, Stack영역의 변수들은 Heap영역의 객체를 참조한다. 이때, Stack 영역의 변수가 GC root가 된다.
Sweep (정리):
- Mark 되지 않은 객체를 식별하고 메모리에서 제거하는 단계
- 이 단계에서 메모리의 불필요한 객체들이 제거되고, 재사용 가능한 메모리가 확보된다.
여기까지가 대부분의 GC에서 기본적으로 수행되고, 일부 알고리즘에서는 추가 단계인 Compact 단계가 존재한다.
Compact (압축):
- 메모리 내의 불규칙하게 살아있는 객체들을 한 곳으로 압축하여 연속적인 메모리 블록으로 만드는 단계
- 이 단계는 메모리 조각화를 방지하고, 효과적으로 메모리를 쓸 수 있게 한다.
Sweep단계의 그림처럼 메모리에 연속적이지 않은 공간을 차지하고 있다면, 배열과 같이 연속된 객체를 만들 수 없을 것이다. 따라서 Compact 단계를 수행하여, 연속된 공간을 확보한다.
Serial Collector (시리얼 콜렉터)
Young 영역과 Old 영역이 연속적으로 처리되며, 싱글 스레드로 GC를 수행하는 Garbage Collector이다. 주로 작은 힙 크기를 가진 단일 스레드 애플리케이션에 적합하다.
초기 Java 버전에서 많이 사용되었다.
알고리즘
- Young 역역 : Mark - Copy
- Old 영역 : Mark-Sweep-Compact
Young 영역은 대부분의 객체가 빠르게 사라지기 때문에, 살아있는 객체를 직접 복사하고 영역을 Clear 하는 방식으로 작동한다. 이런 방식이 Copy 단계이다.
Parallel Collector (병렬 콜렉터)
병렬 콜렉터는 throughput collector(처리량)으로도 알려진 방식이다. 이 방식의 목표는 여러 스레드를 동시에 사용해서, 다중 CPU 환경에서의 애플리케이션 처리량을 높이는 것에 있다.
Java 5, 6에서 주로 사용되었다.
알고리즘
- Young 역역 : Mark - Copy
- Old 영역 : Mark-Sweep-Compact
Young 영역은 Serial Collector와 방식은 동일하지만, 복사 작업이 병렬로 처리된다.
Old 영역은 Serial Collector와 동일한 방식으로 진행된다.
Parallel Compacting Collector (병렬 콤팩팅 콜렉터)
병렬 콜렉터의 Old영역을 GC 할 때, Compact 단계도 멀티 스레드로 수행하는 Collector이다.
알고리즘
- Young 역역 : Mark - Copy
- Old 영역 : Mark-Sweep-Compact
CMS(Concurrent Mark Sweep) Collector (CMS 콜렉터)
Old 영역에 대한 GC를 최적화하기 위해 설계된 Collector이다. 주 목표는 애플리케이션의 일시 중단 시간을 최소화하는 것이다. 그렇기 때문에 상당한 중지 시간을 갖는 Compaction 단계를 수행하지 않고 응답시간을 최소화한다. 하지만 이는 메모리 단편화 문제가 초래하기도 한다.
CMS가 완전히 Compaction을 수행하지 않는다는 것은 아니다. 상황에 따라 Full GC가 발생할 수 있고, 이때 Compaction 이 수행될 수 있다.
CMS GC와 Parallel GC는 Compaction 작업 유무로 구분될 수 있다.
Java 5부터 도입되었으나, Parallel GC에 비해 선호되지 않았다.
알고리즘
- Young 역역 : ParNew
- Old 영역 : Concurrent Mark-Sweep
Concurrent Mark-Sweep(CMS)는 애플리케이션 중단 시간을 줄이기 위해, 아래와 같은 단계로 이루어진다.
- Initial Mark (초기 표시): GC roots에서 직접 액세스 가능한 객체를 표시하는 STW 단계
- Concurrent Mark (병렬 표시): 이 단계에서는 애플리케이션 스레드와 병행하여 살아있는 객체를 표시
- Precleaning: 변경된 객체를 다시 검사하는 단계
- Final Remark (최종 표시): 병렬 표시 중에 변경된 모든 객체를 표시하는 STW 단계
- Concurrent Sweep (병렬 정리): 표시되지 않은 (즉, dead) 객체를 정리하는 병렬 단계
- Concurrent Reset (병렬 재설정): 내부 데이터 구조를 재설정하고 다음 CMS 사이클을 준비
G1( Garbage First ) Collector
CMS GC를 대체하기 위해 새롭게 등장한 Collector이다. 대용량의 메모리가 있는 멀티 프로세서 시스템을 위해 제작되었고, CMS GC보다 효율적으로 동시에 Application과 GC를 진행할 수 있다, 또한, 메모리 Compaction 과정까지 지원한다.
Java 9 버전부터 기본 GC 방식으로 채택되었다.
왜 이름이 G1일까?
G1 GC의 G1은 (Garbage-First)의 약자로, 가장 많은 가비지가 있는 영역부터 수집하는 핵심 전략을 갖고 있다.
이렇게 하면 가장 효율적으로 메모리를 재활용할 수 있으며, 주어진 일시 중지 시간(pause time) 목표 내에서 가능한 한 최대의 메모리를 회수하는 것이 가능해진다.
G1은 어떤 안정성 문제가 있었을까?
G1은 Java 7에서 처음으로 사용 가능하게 되었는데, 당시에 더 오래된 Parallel GC나 CMS GC
와 같은 GC들에 비해 완전히 검증되지 않았다.
초기 버전의 G1 GC는 일부 케이스에서 예상치 못한 행동이나 성능 문제를 일으킬 수 있었다.
어떤 케이스였을까?
1. G1 GC의 주요 목표 중 하나는 일시 중지 시간을 줄이는 것이지만, 초기 버전에서는 일부 상황에서 예상보다 긴 일시 중지 시간이 발생했다.
2. G1은 메모리 조각화 문제를 해결하기 위해 설계되었지만, 초기 버전에서는 여전히 일부 상황에서 조각화 문제가 발생했다.
3. 일부 애플리케이션에서는 G1 GC를 사용할 때 전반적인 성능 저하가 발생했으며, 이는 CMS GC나 Parallel GC와 비교했을 때 특히 두드러졌다.
이러한 문제점들은 시간이 지나면서 Java 업데이트와 함께 개선되었고,
G1 GC는 현재 많은 환경에서 안정적으로 사용되고 있다.
G1은 어떻게 기본 GC가 될 수 있었을까?
G1이 기본 GC로 채택된 것에는 여러 가지 중요 요인들이 있다.
- 예측 가능한 일시 중지 시간:
G1 GC는 일시 중지 시간 목표 (예: -XX:MaxGCPauseMillis)를 설정할 수 있게 설계되었다. 이를 통해 개발자들은 일시 중지 시간을 예측 가능한 범위로 제한할 수 있다. 이러한 특징은 실시간성을 요구하는 애플리케이션에서 특히 중요하다.
- 대규모 힙 지원
- CMS GC의 한계점:
메모리 조각화와 전체 GC로 인한 긴 일시 중지 시간문제 등의 한계점을 해결하기 위해 G1 이 등장했다. - 현대 애플리케이션의 요구 사항 :
대규모 메모리, 실시간 요구 사항 및 효율성을 중시하는 현대 애플리케이션의 요구 사항을 충족시키기 위해 G1 GC가 적합하다고 판단된다.
'Java > Java' 카테고리의 다른 글
[Java] 자바에서 싱글톤(Singleton)패턴을 적용하는 방법 (0) | 2023.12.29 |
---|---|
[Java] 빌더 패턴(Builder Pattern)을 사용하는 이유와 구현 (1) | 2023.12.22 |
[Java] 생성자 대신 정적 팩토리 메서드를 고려하라 (1) | 2023.12.21 |
[Java] Integer.toString()와 String.valueOf()의 차이 (0) | 2023.08.04 |
[Java] 람다식, 함수형 인터페이스와 메소드 참조(::) (0) | 2023.07.14 |