자바 가비지 컬렉션(GC) 란?
가비지 컬렉션(Garbage Collection, GC)은 메모리 관리 기법중 하나로 JVM(자바 가상 머신)의 Heap 영역에서 동적으로 할당했던 메모리 중 필요없게 된 메모리 영역에 할당된 주소들을 모아 주기적으로 제거하는 프로세스를 말한다.
C/C++ 언어에서는 이러한 가비지 컬렉션이 없어 개발자가 수동으로 메모리 할당과 해제를 일일히 해줘야 했다. 반면, JAVA 언어에서는 가비지 컬렉터가 메모리 관리를 대신 수행해주기 때문에 JAVA 프로세스가 한정된 메모리를 효율적으로 사용할 수 있게 하고, 개발자 입장에서는 메모리 관리, 메모리 누수 문제보다 로직 작성에 더 집중할 수 있다는 장점이 있다.
하지만, GC에도 단점이 존재한다. 자동으로 처리해준다 해도 메모리가 언제 해제되는지 정확하게 알 수 없어 제어하기 힘들며, 가비지 컬렉션(GC)이 동작하는 동안에는 다른 동작을 멈추기 때문에 오버헤드가 발생되는 문제점이 있다. 이로 인해 GC가 너무 자주 실행되면 소프트웨어 성능 하락의 문제가 되기도 하다. 이를 STW(Stop-The-World) 라고 한다.
STW(Stop-The-World)
GC를 수행하기 위해 JVM이 프로그램 실행을 멈추는 현상을 말한다. GC가 작동하는 동안 GC관련 Thread를 제외한 모든 Thread는 멈추게 되어 서비스 이용에 차질이 생길 수 있다. 따라서 이 시간을 최소화 하는것이 서비스의 성능에 중요한 영향을 미칠 수 있다.
이런 특성에 따라 실시간 성이 매우 강조되는 프로그램일 경우 가비지 컬렉터(GC)에게 메모리를 맡기는 것은 맞지 않을 수 있다. 실시간 시스템에서 가비지 컬렉션이 사용된다면 치명적인 오류를 발생할 수 있다. 따라서 어플리케이션의 사용성을 유지하면서 효율적이게 GC를 실행하는 최적화 작업이 개발자가 수행해야 하는 중요한 역할이 될 수 있다. 그리고 이러한 GC 최적화 작업을 GC 튜닝이라고 한다.
가비지 컬렉션의 장단점
[장점]
- 메모리 관리를 GC가 수행해주기 때문에 로직 작성 등 개발에 집중할 수 있다.
- GC가 메모리 관리를 대행해 주기 때문에 한정된 메모리를 효율적으로 사용할 수 있다.
[단점]
- 메모리가 언제 해제 되는지 정확히 알 수 없어 제어가 힘들다.
- GC가 동작하는 중에는 JVM이 다른 동작을 멈추기 때문에 오버헤드가 발생한다.
가바지 컬렉션 대상
가비지 컬렉션은 특정 객체가 GC 대상인 Garbage 인지 판단하기 위해서 Reachability라는 개념을 적용한다. 객체가 참조되고 있다면 Reachable로 구분되고, 객체가 참조되고 있지 않다면 Unreachable로 구분해 GC의 대상으로 인식한다.
[Reachable]
객체가 참조되고 있는 상태
[Unreachable]
객체가 참조되고 있지 않으며 GC의 대상이 되는 상태
예를 들어, JVM 메모리에서 객체들은 실질적으로 Heap 영역에서 생성되고 Method 영역 그리고 Stack 영역에서는 Heap 영역에 생성된 객체의 주소만 참조하는 형식으로 구성된다.
하지만, 이렇게 생성된 Heap 영역의 객체들이 메서드가 끝나는 등 특정 이벤트들로 인하여 Heap 영역 객체의 메모리 주소를 가지고 있는 참조 변수가 삭제되는 현상이 발생하면, 위의 그림에서 빨간색 객체와 같이 Heap 영역에서 참조되지 않은 객체(Unreachable)가 발생하게 된다. 이러한 객체들을 주기적으로 가비지 컬렉터가 대상으로 인식하고 할당했던 메모리 영역을 회수한다.
가비지 컬렉션의 동작과정
가비지 컬렉션 청소방식 - Mark And Sweep
가비지 컬렉션은 Unreachable 객체를 청소하기 위해 내부적으로 Mark-Sweep 알고리즘을 사용한다.
Mark-Sweep 알고리즘의 동작방식은 아주 간단하다. 가비지 컬렉션의 대상이 될 객체를 식별(Mark)하고 제거(Sweep)하며 객체가 제거되어 파편화된 메모리 조각들을 앞에서부터 채워 나가는 작업(Compaction)을 수행하게 된다. 이렇게 Mark And Sweep 방식을 사용함면 루트로부터 연결이 끊어진 객체들을 Heap 영역에서 모두 제거할 수 있다.
[Mark 과정]
사용되는 메모리(Reachable)와 사용되지 않는 메모리(Unreachable)을 구분하는 과정이다. Root Space에서 그래프 순회를 통해 연결된 객체를 찾아내어 각각 어떤 객체를 참조하고 있는지 찾아내 마킹한다.
[Sweep 과정]
Mark 단계에서 사용되지 않음(Unreachable)으로 식별된 메모리를 해제하는 작업이다. 참조하고 있지 않은 객체 즉, Unreachable 객체들을 Heap 영역에서 제거한다.
[Compaction 과정]
Sweep 후에 분산된 객체들을 Heap 영역의 시작주소로 모아 메모리가 할당된 부분과 그렇지 않은 부분으로 압축한다. (가비지 컬렉션 종류에 따라 생략되는 경우도 있다.)
[가비지 컬렉션의 Root Space]
Mark And Sweep 방식은 루트로부터 해당 객체에 접근이 가능한지가 해제의 기준이 된다. 자바의 가비지 컬렉션에서의 Root Space는 Heap 메모리 영역을 참조하는 Method Area, Stack, Native Method Stack이 해당된다.
JVM의 Heap 메모리 구조
JVM의 Heap 영역은 JVM의 관리대상이 되는 프로그램 상에서 데이터를 저장하기 위해 런타임시 동적으로 할당하여 사용하는 영역으로 가비지 컬렉션의 대상이 되는 공간이다. 이 공간은 처음 설계될 때 아래의 두 가지 전제 조건 하에 만들어졌다.
- 대부분의 객체는 금방 접근 불가능한 상태가 된다.
- 오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다.
이러한 가설을 Week Generation Hypothesis 라고 한다. 즉, 객체는 대부분 일회성이며 메모리에 오래 남아있는 다는 경우는 드물다는 것이다. 이러한 특성을 이용해 JVM의 메모리 관리를 효율적으로 하기 위해 Heap 영역을 Old와 Young 영역 2가지로 나누어 설계했다.
[Young 영역]
- 새롭게 객체가 할당되는 영역
- 대부분 객체가 금방 Unreachable 상태가 되기 때문에, 많은 객체가 Young 영역에 생성되었다가 사라진다.
- Young 영역에 대한 가비지 컬렉션을 Minor GC라고 부른다.
[Old 영역]
- Young 영역에서 Reachable 상태를 유지하여 살아남은 객체가 복사되는 영역
- Young 영역보다 크게 할당되며, 영역의 크기가 큰 만큼 가비지는 적게 발생한다.
- Old 영역에 대한 가비지 컬렉션을 Major GC 또는 Full GC라고 부른다.
[Old 영역의 크기가 Young 영역보다 큰 이유]
Old 영역이 Young 영역보다 크게 할당되는 이유는 Young 영역의 수명이 짧은 객체들은 큰 공간을 필요로 하지 않으며 큰 객체들은 Young 영역이 아니라 Old 영역에 할당되기 때문이다.
Heap 영역은 효율적인 가비지 컬렉션을 위해 Young 영역을 다시 3가지 영역(Eden, Survivor0, Survivor1)으로 나눈다.
[Eden 영역]
- new 키워드를 통해 새로 생성된 객체가 위치한다.
- 정기적인 가비지 컬렉션 후 살아남은 객체들은 Survivor 영역으로 보내진다.
[Survivor0/Survivor1 영역]
- 최소 1번 이상의 GC에서 살아남은 객체가 존재하는 영역
- Survivor 영역에는 특별한 규칙이 있는데 Survivor0 또는 Survivor1 두 영역 중 하나의 영역은 반드시 비어있어야 한다는 것이다.
Minor GC
Young 영역은 짧게 살아남는 객체들이 존재하는 공간으로 모든 객체는 처음에 Young 영역에 생성된다. Young 영역의 공간의 Old 영역의 공간에 비해 상대적으로 작기 때문에 메모리 상의 객체를 찾아 제거하는데 적은 시간이 걸리고 이 때문에 Young Generation 영역에서 발생되는 GC를 Minor GC라고 부른다.
[Minor GC 동작과정 요약]
- 새로 생성된 객체가 Eden 영역에 할당된다.
- 객체가 계속 생성되어 Eden영역이 가득차면 Minor GC가 실행된다.
- Eden 영역에서 살아남은 객체는 1개의 Survivor 영역(Survivor0 또는 Survivor1)으로 이동한다.
- Eden 영역에서 사용되지 않는 객체(Unreachable)의 메모리가 해제(Sweep)된다.
- 1~2의 과정을 반복하며 Survivor0과 Survivor1 영역을 번갈아가며 사용한다. (1개의 Survivor 영역은 반드시 비어있다.)
- 이러한 과정을 반복하며 계속해서 살아남은 객체는 Old 영역으로 이동한다.
[Minor GC 동작과정 상세]
1. 새로 생성된 객체가 Eden 영역에 할당된다.
2. 객체가 계속 생성되어 Eden영역이 가득차면 Minor GC가 실행된다.
3. Mark 동작을 통해 Reachable 객체를 탐색한다.
4. Eden 영역에서 살아남은 객체는 1개의 Survivor 영역으로 이동한다.
5. Eden 영역에서 사용되지 않는 객체(Unreachable)를 해제(Sweep)한다.
6. 살아남은 객체의 age 값이 1씩 증가한다.
7. 또 다시 Eden 영역에 신규 객체들로 가득 차게 되면 다시 한번 Minor GC가 발생하고 Mark 작업을 수행한다.
8. Mark 한 객체들을 비어있는 Survivor1 영역으로 이동시키고 Sweep 작업을 수행한다.
9. 살아남은 객체들의 age 값을 1씩 증가한다.
10. 위 1~9까지의 작업을 반복한다.
[age 값]
Survivor 영역에서 객체가 살아남은 횟수를 의미하는 값으로 Object Header에 기록된다. 만일 age 값이 임계값에 다다르면 Promotion(Old 영역으로 이동) 여부를 결정한다. JVM 중 가장 일반적인 Hotspot JVM의 경우 age의 기본 임계값은 31이다. 객체 헤더에 age를 기록하는 부분이 64bit로 되어 있기 때문이다.
또한, Survivor 영역의 제한조건으로 Survivor 영역 중 반드시 1개는 사용되어야 하고 나머지는 비어있어야 한다. 만약, 두 Survivor 영역에 모두 데이터가 존재하거나 모두 사용량이 0이라면 현재 시스템이 정상적이지 않다는 반증이 된다.
Major GC
Old 영역은 길게 살아남은 객체들이 존재하는 공간이다. Old 영역의 객체들은 처음에는 Young 영역에서 생성되었으나 GC 과정에서 제거되지 않은 경우 age 값이 임계값에 도달하게 되어 이동된 객체들이다. 그리고 Major GC는 객체들이 계속 Promotion 되어 Old 영역의 메모리가 부족하게 되면 실행된다.
[Major GC 동작과정]
1. 객체의 age 값이 임계값에 도달하는 경우 Promotion이 발생한다. (여기서는 임계값이 9로 가정)
2. 해당 객체들이 Old 영역으로 이동한다. 이를 Promotion 이라고 한다.
3. 위 과정이 반복되어 Old 영역의 메모리가 부족하게 되면 Major GC가 동작한다.
Major GC는 Old 영역의 데이터가 가득차게 되면 발생하는 단순한 방식이다. Old 영역에 할당된 메모리가 허용치를 넘게되면 Old 영역에 있는 모든 객체들을 검사하여 참조되지 않는 객체들을 한꺼번에 삭제하는 Major GC가 실행된다. 하지만, Old 영역은 Young 영역에 비해 큰 공간을 가지고 있기 때문에 메모리 상의 객체 제거에 많은 시간이 걸리게 된다.
예를 들어, Young 영역은 일반적으로 Old 영역보다 크기가 작기 때문에 GC가 보통 0.5초에서 1초사이에 끝나게 된다. 따라서, Minor GC는 애플리케이션에 크게 영향을 주지 않는다. 하지만, Old 영역의 Major GC는 일반적으로 Minor GC보다 시간이 오래걸리며 10배 이상의 시간을 사용한다. 바로 여기서 Stop-The-World 문제가 발생하게 된다.
Major GC가 일어나게 되면 Thread 가 멈추고 Mark And Sweep 작업을 통해 CPU에 부하를 주기 때문에 멈추거나 버벅임 증상이 발생하게 된다. 따라서, 자바 개발자들은 계속해서 가비지 컬렉션 알고리즘을 발전시켜 성능향상을 위해 노력해 왔다.}
다양한 가비지 컬렉션 알고리즘
자바가 발전함에 따라 Heap 사이즈가 커지게 되었고 애플리케이션의 지연현상이 두드러지게 되었다. 이를 최적화하기 위해 다양한 가비지 컬렉션 알고리즘이 개발되었다. 지금부터 소개할 GC 알고리즘은 모두 설정을 통해 Java에 적용이 가능하다. 즉, 상황에 따라 필요한 GC 알고리즘을 사용할 수 있다.
Serial GC
- Java SE 5, 6에서 디폴트로 사용되는 GC이다.
- 서버의 CPU 코어가 1개일 경우 사용하기 위해 개발된 가장 단순한 GC이다.
- GC를 처리하는 스레드가 1개(싱글 스레드)여서 가장 stop-the-world 시간이 길다.
- Minor GC에는 Mark-Sweep을 사용하고 Major GC에서는 Mark-Sweep-Compact를 사용한다.
- 보통 실무에서 사용하지 않는다. (디바이스 성능이 안좋아서 CPU 코어가 1개일 경우 사용)
Parallel GC
- Java 8의 디폴트 GC이다.
- Serial GC와 기본적인 알고리즘은 같지만 Young 영역의 Minor GC를 멀티 스레드로 수행한다.
- Serial GC에 비해 stop-the-world 시간이 짧다.
Parallel Old GC
- Parallel GC에서 Old 영역에 새로운 알고리즘을 추가한 버전의 GC이다.
- Young 영역 뿐만 아니라 Old 영역에서도 멀티 스레드로 GC를 수행한다.
- 새로운 가비지 컬렉션 청소 방식인 Mark-Summary-Compact 방식을 이용한다.
CMS GC
- 어플리케이션의 스레드와 GC 스레드가 동시에 실행되어 stop-the-world 시간을 최대한 줄이기 위해 고안된 GC
- GC 대상을 파악하는 과정이 복잡한 여러 단계로 수행되기 때문에 다른 GC 대비 CPU 사용량이 높다.
- 메모리 파편화 문제가 있다.
- Java 9 버전부터 decrecated 되었고 결국 Java 14 버전에서 사용이 중지되었다.
G1 GC (Garbage First)
- CMS GC를 대체하기 위해 Java 7 버전에서 최초로 고안된 GC로 Java 9+ 버전의 기본 GC로 지정되었다.
- 4GB 이상의 힙 메모리, stop-the-world 시간이 0.5초 정도 필요한 상황에 사용하기 적합하다. (Heap이 너무 작을경우 미사용 권장)
- Region 이라는 새로운 개념을 도입했다.
- 기존의 GC 알고리즘에서는 Heap 영역을 물리적으로 고정된 Young/Old 영역으로 나누어 사용했지만 G1 GC는 전체 힙 영역을 Region 이라는 영역으로 체스판과 같이 분할하여 상황에 따라 Eden, Survivor, Old 영역 등 역할을 고정이 아닌 동적으로 부여한다. 가비지로 가득 찬 영역을 빠르게 회수하여 빈 공간을 확보하기 때문에 결국 GC 빈도가 줄어드는 효과를 얻게 되는 원리이다.
Shenandoah GC
- Java 12 버전에서 release 되었다.
- 래드햇에서 개발한 GC로 큰 GC 작업을 여러번 수행하는 것보다 작은 GC 작업을 여러번 수행하는게 더 좋다는 개념을 적용하여 만든 GC이다.
- 기존 CNS GC가 갖는 단편화 문제와 G1 GC가 갖는 pause 이슈를 모두 해결했다.
- 강력한 Concurrency와 가벼운 GC 로직으로 heap 사이즈에 영향을 받지 않고 일정한 pause 소요 시간이 특징이다.
ZGC (Z Garbage Collector)
- Java 15에서 release
- 대량의 메모리(8GB~16TB)를 low-latency로 잘 처리하기 위해 고안된 GC
- G1 Region처럼 ZGC는 ZPage라는 영역을 사용하며 G1 Region은 크기가 고정인데 비해 ZGC의 ZPage는 2mb 배수로 동적으로 운용된다. (큰 객체가 들어오면 2의 제곱으로 영역을 구성해서 처리한다.)
- ZGC가 내세우는 최대 장점 중 하나는 Heap의 크기가 증가하더라도 stop-the-world의 시간이 절대 10ms를 넘지 않는다는 것이다
Reference
[Naver D2] Java Garbage Collection
[inpa] 가비지 컬렉션 동작 원리 & GC 종류 정리
[jellili] [Java] 가비지 컬렉션(GC, Garbage Collection)