Garbage Collection의 동작 방식과 종류
Garbage Collection
"JVM의 가비지 컬렉터가 불필요한 메모리 자원을 해제하는 작업"
자바에서 명시적으로 불필요한 데이터를 표현하기 위해서 null을 선언할 수 있다.
허나, 이러한 패턴의 코드는 잘 보이지 않는다.
가독성에도 좋지 않지만 null을 선언하지 않아도 가비지 컬렉터의 알고리즘에 의해 수집 될 수 있다.
가비지 컬렉션은 객체를 참조하는 다른 자원이 있는지 확인하는 Reachable 작업을 통해 제거 대상을 판단한다.
가비지 컬렉션의 대상인 힙 영역의 객체들은 스택 영역의 변수들이나 참조타입의 정적 필드 의해 참조된다.
스택이나 정적 필드에 의해 참조되는 객체를 Reachable이라고 하고, 참조되지 않는 객체는 UnReachable이라고 한다.
가비지 컬렉션은 UnReachable이라고 판단한 객체는 재사용 가능성이 낮다고 생각하여 해당 객체의 메모리 점유를 해제시킨다.
* 가비지 컬렉터 : 메모리 자원을 할당하고 해제(GC)하는 프로그램
Garbage Collection의 알고리즘 종류
1. Mark & Sweep
스택의 변수나 정적필드로부터 참조되는 객체를 시작점(Root Space)으로 참조하는 다른 객체들을 Mark 한다.
Mark한 객체들은 Reachable이 되고 마킹되지 못한 객체들은 UnReachable이 되므로 UnReachable 객체의 메모리 자원을 해제하는 Sweep 과정을 실행한다.
2. Mark Sweep Compact
Mark & Sweep 이후에 Compact를 진행한다. Compact는 메모리 단편화를 해결하기 위한 작업이다.
Compact 작업에서는 Reachable로 살아 남은 객체들을 Heap의 시작 주소로 모아 연속적으로 재배치하여 메모리 단편화 문제를 해결한다.
주로 객체들의 메모리 영역 이동 시, 새로운 영역의 메모리 주소 앞부분부터 채워넣는 compact 작업이 수행된다.
(* 메모리 단편화 : 메모리 할당 및 해제 작업의 반복으로 중간중간 작은 메모리가 존재하지만 연속적으로 존재하지 않아
새로운 자원을 할당하지 못하는 경우를 말한다. )
3. Mark Summary Compact
Mark Sweep Compact 알고리즘을 따르며 Summary를 통해 Compact 대상을 선정하는 알고리즘이다.
메모리 할당 밀도를 분석하여 밀도가 높은 부분은 메모리 파편화가 적고 밀도가 낮은 영역은 메모리 파편화가 높다고 판단하여 메모리 파편화 가능성이 높은 영역에 Compact 작업을 집중한다.
* Stop The World
Stop The World는 가비지 제거에 사용되는 알고리즘은 아니며 GC 스레드를 제외한 나머지 스레드를 중단 시키고 GC 작업을 수행하는 것을 말한다.
Stop The World를 진행하는 이유
compact 작업으로 발생 할 수 있는 동시성 문제로 인해 STW를 수행한다.
compact 작업은 주로 영역 이동을 하며 이뤄지는데 이때 메모리 주소가 바뀐다.
애플리케이션 스레드에서 현재 참조 중인 주소도 동시에 바꿔줘야하기에 이 작업이 정상적으로 진행되지 않으면 동시성 문제 발생 가능성이 있다.
JVM의 young 영역은 old 영역에 비해 크기가 작아 빠르게 수행될 수 있어 동시성 문제를 유발할 가능성이 낮지만
old 영역은 메모리 영역 크기가 크기에 상대적으로 동시성 문제 유발 가능성이 높아 STW가 주로 수행된다.
Heap Space별 GC
먼저 JVM의 Heap 영역이 Young과 Old로 나뉘게 된 배경을 보자.
- 대부분의 객체는 금방 접근 불가능한 상태(Unreachable)가 된다.
-> young 영역에 집중해서 GC 작업이 이루어짐 - 오래된 객체에서 새로운 객체로의 참조는 아주 적게 존재한다.
-> young 영역 객체 mark 작업 수행시 old 영역의 객체를 시작점으로 young과 참조 관계 있는지 탐색하지 않음
(카드 테이블 활용)
위와 같은 이유로 Young 영역과 Old 영역으로 나뉘었고 Young 영역의 사이즈를 Old 보다 작게 유지한채 Young 영역에 집중해서 메모리 할당과 해제가 이뤄지도록 하였다.
카드 테이블
Old 영역에 있는 객체가 Young 영역의 객체를 참조할 때마다 정보가 표시된다.
young 영역의 GC를 실행할 때 Old 영역에 있는 모든 객체의 참조를 확인하지 않고, 카드 테이블만 탐색하여 GC 대상인지 식별한다.
Young 영역 : Minor GC
Minor GC는 young 영역에서 동작하며 Mark Sweep Compact 알고리즘을 따른다.
GC 작업을 수행 이후에도 Eden영역이 꽉차면살아남은 객체를 Survivor0으로 보낸다.
Survivor0가 가득 차게 되면 Survivor1으로 보낸다. Survivor 영역에서도 지속적으로 가비지 컬렉션 작업이 이뤄지며
S0와 S1을 반복적으로 이동하며 compact 작업이 수행되고 임계치에 도달한 객체들은 Old 영역으로 이동된다.
(대부분의 객체는 생성 이후 다른 객체에 의해 참조될 일이 없으므로, 많은 객체가 Young 영역에 생성되었다가 사라진다.)
객체의 age
Survivor 영역에서 객체가 살아남은 횟수를 age라고 하며 age 값이 임계값에 도달하면 Old 영역으로 이동하게 된다.
HotSpot JVM의 경우 임계값이 31로 설정되어있다.(age는 Object Header에 기록됨)
Old 영역 : Major GC
Old 영역은 Young 영역에서 GC에 의해 제거되지 않고 살아남은 객체가 복사되는 영역으로 Major GC의 처리를 받는다.
Old 영역의 경우 young 영역에 비해 크기가 큰 만큼 GC의 작업량이 많아 Stop the world 부하가 상대적으로 훨씬 크다.
자바 8 이전까지 Mark Sweep Compact 알고리즘을 사용하였으나, 이후에는 Mark Summary Compact 알고리즘을 사용하는 G1GC를 사용함으로써 STW의 부하를 줄였다.
Garbage Collection 종류
Old 영역의 가비지 수집을 위한 Stop the world의 부하로 가비지 컬렉션은 여러 알고리즘이 적용되어 발전되어왔다.
각각의 종류에 대해 알아보자.
Serial GC(직렬 가비지 컬렉션)
Serial GC는 Young 영역에서 Mark & Sweep으로 Old 영역에서 Mark Sweep Compact를 사용한다.
Serial GC에서 가비지 컬렉션을 수행하는 스레드는 하나로 단일 코어 CPU 환경에서 주로 사용된다.
CPU 코어가 여러개이더라도 GC 스레드가 1개이므로 가비지 컬렉션 작업에서 효율성의 차이는 존재하지 않는다.
Serial GC는 자바 1.3 이전 버전에서 디폴트로 사용되었다.
Parallel GC(병렬 가비지 컬렉션)
"다중 코어 CPU의 탄생"으로 GC에 병렬 처리 개념이 적용되었다.
Parallel GC의 알고리즘은 Serial GC의 알고리즘과 같지만, GC를 여러개의 스레드가 처리하는 병렬성을 갖고 있다.
당연히 GC 작업의 병렬 처리가 가능하므로 Serial GC에 비해 수행 능력이 뛰어나지만 Young 영역에서 사용하는 Minor GC에만 적용되고 Old 영역에서는 여전히 단일 스레드가 사용되었다.
Parallel Old GC(병렬 Old 영역 가비지 컬렉션)
Mark Summary Compact 방식으로 Old 영역에서 병렬 처리를 진행한다.
허나, Old영역 병렬 처리의 복잡성과 성능 저하로 java에서 default로 적용됬던 적은 없다.
CMS GC(Concurrent Mark Sweep)
Mark Sweep을 하는 GC 스레드와 애플리케이션 작업을 하는 스레드가 동시에 작업을 진행한다.
Compact 작업을 진행하지 않기에 애플리케이션 스레드와 Mark & Sweep만 진행하는 GC가 동시에 수행되더라도 동시성 문제는 발생하지 않지만 메모리 파편화 문제는 해결할 수 없다.
CMS 방식은 Stop the World 작업을 줄이기 위한 방식이지만 메모리 파편화 문제로 인해 오히려 비효율이 나타나,
결국 자바 9버전 부터 deprecated 되었고 14부터 사용중지 되었다.
G1 GC(Garbage First)
이전의 Old 영역 병럴 처리 문제로 인한 성능 저하를 리전이라는 개념으로 개선한 알고리즘이다.
"메모리 용량 스펙 향상"으로 힙 영역을 여러개의 리전으로 나누어도 하나의 리전에 충분히 객체를 저장할 수 있어 리전 개념이 도입됬다.
G1 GC는 힙 영역을 작은 여러개의 균등한 리전으로 나누어 각 영역에서 GC 작업이 이뤄지게 하여
GC작업이 여러번 일어날 수 있지만 Stop the World 시간을 짧게 가져가 애플리케이션이 GC 작업으로 인한 멈춤 현상을 짧게 가져가도록 처리하였다.
G1 GC의 특징
- 힙을 여러개의 작은 리전으로 나누어 메모리를 할당하고 해제함
GC 작업이 자주 발생할 수 있지만 빠르게 끝낼 수 있어 Old 영역에서 Stop the World에서 애플리케이션이 멈추는 문제를 해결하는데 큰 도움이 된다. - 리전 밀도가 높은 영역 우선 수행
(메모리 파편화 가능성이 높은 영역) - Available/Unused(사용 가능한 비어있는) 영역의 추가
Available/Unused라는 비어있는 리전을 만들어 필요에 따라 Eden, Survivor, Old로 사용하도록 함
이를 통해 애플리케이션은 필요에 따라 young 영역과 old 영역의 크기를 동적으로 할당하여 애플리케이션 사용성에 따라 메모리 영역 크기를 스스로 최적화할 수 있음 - Humongonous 영역 추가
리전의 크기의 50%가 넘는 Humongonous 객체들을 할당하는 공간으로 여러 연속적인 리전에 저장될 수 있다.
[참고] G1 GC는 자바 9부터 디폴트로 사용되었다.