[JAVA] GC (Garbage Collector)

D A S H B O A R D
D E V E L O P
S E C U R I T Y
 GC(Garbage Collector)
 GC의 대상(Garbage)
 GC의 청소 방법
 GC의 동작 과정
 GC 알고리즘 종류
Reference

 GC(Garbage Collector)

가비지 컬렉션(Garbage Collection, 이하 GC)은 자바의 메모리 관리 방법 중의 하나로 JVM(자바 가상 머신)의 Heap 영역에서 동적으로 할당했던 메모리 중 필요 없게 된 메모리 객체(garbage)를 모아 주기적으로 제거하는 프로세스를 말한다.
Java에서는 가비지 컬렉터가 메모리 관리를 대행해주기 때문에 Java 프로세스가 한정된 메모리를 효율적으로 사용할수 있게 하고, 개발자 입장에서 메모리 관리, 메모리 누수(Memory Leak) 문제에서 대해 관리하지 않아도 되어 오롯이 개발에만 집중할 수 있다는 장점이 있다.
단점 - Stop-The-World
메모리가 언제 해제되는지 정확하게 알 수 없기 때문에, 제어하기 힘들다.
가비지 컬렉션(GC)이 동작하는 동안에는 다른 동작을 멈추기 때문에 오버헤드가 발생된다.
위와 같은 현상을 Stop-The-World(STW) 라고 한다.
GC를 수행하기 위해 JVM이 프로그램 실행을 멈추는 현상을 의미
GC가 작동하는 동안 GC 관련 Thread를 제외한 모든 Thread는 멈추게 되어 서비스 이용에 지장

 GC의 대상(Garbage)

Garbage를 알기 위해서는 Heap Area의 설계 전제를 알아야한다. JAVA의 Heap Area는 2가지 전제를 기반으로 설계되었는데, 첫번째 대부분의 객체는 금방 접근 불가능한 상태(Unreachable)가 된다. 두번째, 오래된 객체에서 새로운 객체로의 참조는 아주 적게 존재한다.
위와 같은 전제를 바탕으로 GC는 Reachable(도달성)을 기준으로 Garbage를 판단한다.
객체에 레퍼런스가 있다면 Reachable로 구분되고, 객체에 유효한 레퍼런스가 없다면 Unreachable로 구분해버리고 수거해버린다.
Reachable : 객체가 참조되고 있는 상태
Unreachable : 객체가 참조되고 있지 않은 상태 == Garbage
JVM에서 객체는 Heap Area에 생성되고, Stack Area와 Method Area에서 해당 객체 주소를 참조하는 식으로 구성된다.
이렇게 생성된 Heap Area의 객체들이 메서드가 끝나는 등의 특정 이벤트들로 인하여 Heap Area 객체의 메모리 주소를 가지고 있는 참조 변수가 삭제되는 현상이 발생하게 된다면, Unreachable한 객체로서 GC의 대상이 된다.

 GC의 청소 방법

Mark and Sweep 은 다양한 GC에서 사용되는 객체를 솎아내는 내부 알고리즘이다.
가비지 컬렉션이 될 대상 객체를 식별(Mark)하고 제거(Sweep)하며 객체가 제거되어 파편화된 메모리 영역을 앞에서부터 채워나가는 작업(Compaction)을 수행
Mark  : 먼저 Root Space로부터 그래프 순회를 통해 연결된 객체들을 마킹
Sweep : 참조하고 있지 않은 객체 즉 Unreachable 객체들을 Heap에서 제거
Compact : Sweep 후에 분산된 객체들을 Heap의 시작 주소로 모아 메모리가 할당된 부분과 그렇지 않은 부분으로 압축 (가비지 컬렉터 종류에 따라 하지 않는 경우도 있음)
Root Space
JVM GC에서의 Root Space는 Heap 메모리 영역을 참조하는 method area, static 변수, stack, native method stack이 되게 된다.

 GC의 동작 과정

Heap 메모리 구조

Heap영역은 처음 설계될 때 다음의 2가지를 전제 (Weak Generational Hypothesis)로 설계됨
대부분의 객체는 금방 접근 불가능한 상태(Unreachable)가 된다.
오래된 객체에서 새로운 객체로의 참조는 아주 적게 존재한다.
즉, 객체는 대부분 일회성되며, 메모리에 오랫동안 남아있는 경우는 드물다는 것
이러한 특성을 이용해 JVM 개발자들은 보다 효율적인 메모리 관리를 위해, 객체의 생존 기간에 따라 물리적인 Heap 영역을 나누게 되었고 Young 과 Old 총 2가지 영역으로 설계

Young Generation

자바 객체가 생성되자마자 저장되고, 생긴지 얼마 안되는 객체가 저장되는 공간
대부분의 객체가 금방 Unreachable 상태가 되기 때문에, 많은 객체가 Young 영역에 생성되었다가 사라진다.
Young Generation과 Tenured Generation 에서의 GC를 Minor GC라고 함

Young Generation Detail

Eden
new를 통해 새로 생성된 객체가 위치
정기적인 쓰레기 수집 후 살아남은 객체들은 Survivor 영역으로 보냄 (Minor GC)
Servivor0 / Servivor1
최소 1번의 GC 이상 살아남은 객체가 존재하는 영역
Survivor 0 또는 Survivor 1 둘 중 하나에는 꼭 비어 있어야 함!!!

Old(Tenured) Generation

Young Generation에서 Reachable 상태를 유지하여 살아남은 객체가 복사되는 영역
Young 영역보다 크게 할당되며, 영역의 크기가 큰 만큼 가비지는 적게 발생
Old영역의 메모리를 회수하는 GC를 Major GC라 함
Old 영역이 Young 영역보다 크게 설계된 이유
Young 영역의 수명이 짧은 객체들은 큰 공간을 필요로 하지 않으며 큰 객체들은 Young 영역이 아니라 바로 Old 영역에 할당
Permanent
생성된 객체들의 정보의 주소값이 저장된 공간
Method Area의 메타정보가 기록된 곳이며 JVM에 의해 사용되었음
Java8부터 제거되고 Native Method Stack에 편입

Minor GC

Young Generation 영역은 짧게 살아남는 메모리들이 존재하는 공간이며 처음 생성된 모든 객체들이 저장되는 영역이다.
Young Generation의 공간은 Old Generation에 비해 상대적으로 크기가 작기 때문에 메모리 상의 객체를 찾아 제거하는데 적은 시간이 걸린다. 
이 때문에 Young Generation 영역에서 발생되는 GC를 Minor GC라 불린다.
1.
객체가 생성되면 최초로 Eden 영역에 할당
2.
객체가 계속 생성되어 Eden 영역이 꽉차게 되고 Minor GC가 실행
3.
Mark 동작을 통해 Reachable 객체를 탐색 → Garbage 걸러내는 작업
4.
Eden 영역에서 살아남은 객체는 1개!!의 Survivor 영역으로 이동
5.
Sweep 동작을 통해 Unreachable 객체들의 메모리 해제 작업
6.
살아남은 객체들에 대해서 Age 주는 작업 → Age를 보고 Old 영역으로 Promotion 할지 결정
7.
또 다시 Eden 영역이 꽉차게 되고 Minor GC가 실행
8.
Mark 동작을 통해 Reachable 객체를 탐색 - Garbage 걸러내는 작업
9.
Eden 영역에서 살아남은 객체와 Servivor 영역에서 살아남은 객체는 비어있는 Servivor 영역으로 이동
10.
Sweep 동작을 통해 Unreachable 객체들의 메모리 해제 작업
11.
살아남은 객체들에 대해서 Age 주는 작업
해당 과정이 계속해서 반복하며 Minor GC를 진행하며, Young Generation에서 age가 임계값에 도달했을 경우에는 Old Generation으로 이동하게 된다.
Promotion 여부 결정 방법 - 객체의 생존 횟수를 카운트 - age
Minor GC에서 객체가 살아남은 횟수를 의미하는 age를 Object Header에 기록
그리고 Minor GC 때 Object Header에 기록된 age를 보고 Promotion 여부를 결정
JVM 중 가장 일반적인 HotSpot JVM의 경우 이 객체 헤더에 age를 기록하는 부분이 6 bit로 되어 있기 때문에 age의 기본 임계값은 31이다.
Premature Promotion
Eden 영역과 Survivor 영역이 꽉찼지만, age도 임계치에 도달하지 않았을 경우 일어남
이 때, age가 임계치에 도달하지 않아도 Old generation으로 이동 된다.
Survivor 영역 중 1개는 반드시 사용이 되어야 함
만약 두 Survivor 영역에 모두 데이터가 존재하거나, 모두 사용량이 0이라면 현재 시스템이 정상적인 상황이 아님을 파악

Major GC

Old Generation에는 길게 생존된 객체들이 저장된다.
이러한 객체들은 위 Minor GC 과정에서 age를 증가시키며 판단하며, Young Generation에서 age가 임계값에 다다를 경우 Old로 넘어오게 된다.
그리고 Major GC는 객체들이 계속 Promotion되어 Old 영역의 메모리가 부족해지면 발생한다. 이는, Minor GC보다 시간이 오래걸리며, 10배 이상의 시간을 사용한다.
Major GC + Minor GC == Full GC
1.
Young Generation의 객체 age 값이 임계치에 도달 → 디폴트 age 임계값인 31로 설정
2.
임계치에 도달한 객체를 Old Generation으로 이동 → Promotion
3.
위 과정들이 반복하며 Old Generation이 가득 찰 경우 Major GC 발생
Major GC는 Old 영역은 데이터가 가득 차면 GC를 실행하는 단순한 방식
Old 영역에 할당된 메모리가 허용치를 넘게 되면, Old 영역에 있는 모든 객체들을 검사하여 참조되지 않는 객체들을 한꺼번에 삭제하는 Major GC가 실행되게 된다.
하지만 Old Generation은 Young Generation에 비해 상대적으로 큰 공간을 가지고 있어, 이 공간에서 메모리 상의 객체 제거에 많은 시간이 걸리게 된다. (대략 10배 정도)
Stop-The-World가 발생
Major GC가 일어나면 CPU에 부하를 주기 때문에 멈추거나 버벅이는 현상이 일어나, Mark and Sweep 작업을 GC Thread 외에 모든 Thread를 중지시킨다.

 GC 알고리즘 종류

JVM의 GC는 개발자들이 메모리 관리를 해주지 않기 때문에 큰 장점을 가져다 주지만, Stop The World가 발생되어 애플리케이션이 중지되는 문제점이 발생한다.
또한, Heap 사이즈가 커지면서 Suspend 현상이 잦게 일어나 GC를 최적화할 수 있는 알고리즘이 많이 나왔다.

Serial GC

서버의 CPU 코어가 1개일 때 사용하기 위해 개발된 가장 단순한 GC
GC를 처리하는 쓰레드가 1개 (싱글 쓰레드) 이어서 가장 stop-the-world 시간이 길다
Minor GC 에는 Mark-Sweep을 사용하고, Major GC에는 Mark-Sweep-Compact를 사용한다.
보통 실무에서 사용하는 경우는 없다 (디바이스 성능이 안좋아서 CPU 코어가 1개인 경우에만 사용)
실행방법
-XX:+UseSerialGC GC 옵션을 지정
java -XX:+UseSerialGC -jar Application.java
Bash
복사

Parallel GC

Java 8의 디폴트 GC
Serial GC와 기본적인 알고리즘은 같지만, Young 영역의 Minor GC를 멀티 쓰레드로 수행 (Old 영역은 여전히 싱글 쓰레드)
Serial GC에 비해 stop-the-world 시간 감소
실행방법
GC 스레드는 기본적으로 cpu 개수만큼 할당
옵션을 통해 GC를 수행할 쓰레드의 갯수 등을 설정 가능
java -XX:+UseParallelGC -jar Application.java # -XX:ParallelGCThreads=N : 사용할 쓰레드의 갯수
Bash
복사

Parallel Old GC (Parallel Compacting Collector)

Parallel GC를 개선한 버전
Young 영역 뿐만 아니라, Old 영역에서도 멀티 쓰레드로 GC 수행
새로운 가비지 컬렉션 청소 방식인 Mark-Summary-Compact 방식을 이용
실행방법
java -XX:+UseParallelOldGC -jar Application.java # -XX:ParallelGCThreads=N : 사용할 쓰레드의 갯수
Bash
복사

CMS GC (Concurrent Mark Sweep)

어플리케이션의 쓰레드와 GC 쓰레드가 동시에 실행되어 stop-the-world 시간을 최대한 줄이기 위해 고안된 GC
단, GC 과정이 매우 복잡해짐.
GC 대상을 파악하는 과정이 복잡한 여러단계로 수행되기 때문에 다른 GC 대비 CPU 사용량이 높다
메모리 파편화 문제
CMS GC는 Java9 버젼부터 deprecated 되었고 결국 Java14에서는 사용이 중지
실행방법
java -XX:+UseConcMarkSweepGC -jar Application.java
Bash
복사

G1 GC (Garbage First)

GC의 대부분 작업을 애플리케이션의 스레드를 멈추지 않고도 동시에 진행할 수 있으며 두 번째로는 연속적으로 공간을 가지는 것이 아니라는 점에서 매우 큰 힙 영역을 다루는데에 있어 효율적(CMS의 메모리 단편화 해결 가능) 또한 목적으로는 높은 처리량 + 낮은 지연율 + STW의 일정된 시간 유지가 있다.
Humonogous: Region 크기의 50%를 초과하는 큰 객체를 저장하기 위한 공간
CMS GC를 대체하기 위해 jdk 7 버전에서 최초로 release된 GC
Java 9+ 버전의 디폴트 GC로 지정
4GB 이상의 힙 메모리, Stop the World 시간이 0.5초 정도 필요한 상황에 사용 (Heap이 너무작을경우 미사용 권장)
기존의 GC 알고리즘에서는 Heap 영역을 물리적으로 고정된 Young / Old 영역으로 나누어 사용하였지만, G1 GC는 아예 이러한 개념을 뒤엎는 Region이라는 개념을 새로 도입하여 사용.
전체 Heap 영역을 Region이라는 영역으로 체스같이 분할하여 상황에 따라 Eden, Survivor, Old 등 역할을 고정이 아닌 동적으로 부여
실행방법
java -XX:+UseG1GC -jar Application.java
Bash
복사
G1 GC의 효율성
일일이히 메모리를 탐색하는 것이 아닌, 메모리가 많이 차있는 영역(region)을 인식하는 기능을 통해 메모리가 많이 차있는 영역을 우선적으로 GC
즉, Heap Memory 전체를 탐색하는 것이 아닌 영역(region)을 나눠 탐색하고 영역(region)별로 GC가 일어난다.
또한, G1 GC는 더욱 효율적이라고 생각하는 위치로 객체를 Reallocate(재할당)
예) 이전 방법으로는 Eden → Servivor 였지만, Servivor에 있다가 Eden에 있는 것이 더 효율적이라 판단될 경우 Servivor → eden으로 이동한다.
G1 GC의 장단점
장점
별도의 STW 없이도 여유 메모리 공간을 압축하는 기능을 제공
Generation의 일부분 Region에 대해서만 Compaction 가능
Garbage로 가득찬 영역을 빠르게 회수하여 빈 공간을 확보하므로, 결국 GC 빈도가 줄어드는 효과를 얻게 되는 원리
단점
공간 부족 상태를 조심해야 한다. (Minor GC, Major GC 수행하고 나서도 여유 공간이 부족한 경우)
이때는 Full GC가 발생하는데, 이 GC는 Single Thread로 동작한다.
Full GC는 heap 전반적으로 GC가 발생하는 것을 뜻한다.
작은 Heap 공간을 가지는 Application에서는 제 성능을 발휘하지 못하고 Full GC가 발생
Humonogous 영역은 제대로 최적화되지 않으므로 해당 영역이 많으면 성능이 떨어짐

Shenandoah GC

Java 12에 release
레드 햇에서 개발한 GC
기존 CMS가 가진 단편화, G1이 가진 pause의 이슈를 해결
강력한 Concurrency와 가벼운 GC 로직으로 heap 사이즈에 영향을 받지 않고 일정한 pause 시간이 소요가 특징
실행방법
java -XX:+UseShenandoahGC -jar Application.java
Bash
복사

ZGC (Z Garbage Collector)

Java 15에 release
대량의 메모리(8MB ~ 16TB)를 low-latency로 잘 처리하기 위해 디자인 된 GC
G1의 Region 처럼, ZGC는 ZPage라는 영역을 사용하며, G1의 Region은 크기가 고정인데 비해, ZPage는 2mb 배수로 동적으로 운영됨. (큰 객체가 들어오면 2^ 로 영역을 구성해서 처리)
ZGC가 내세우는 최대 장점 중 하나는 힙 크기가 증가하더도 'stop-the-world'의 시간이 절대 10ms를 넘지 않는다는 것
실행방법
java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -jar Application.java
Bash
복사