Java

[Java] 가비지 컬렉션(Garbage Collection)

이덩우 2024. 1. 12. 02:27

Garbage Collection(GC)란?

가비지 컬렉션(이하 GC)은 자바의 메모리 관리 기법 중의 하나로, JVM의 *Heap 영역*에서 동적으로 할당되었던 메모리 중 *더 이상 사용하지 않는 객체를 모아 주기적으로 제거*하는 프로세스를 뜻한다.

C 계열의 언어에는 이러한 가비지 컬렉션이 없어 개발자가 스스로 메모리를 할당하고 해제까지 해줘야한다.

반면, Java에서는 가비지 컬렉션이 메모리 관리를 대행해주기 때문에 개발자는 오롯이 *개발에만 집중*할 수 있다는 장점이 있다.

 

GC의 개념은 꼭 자바에만 있는건 아니다.

파이썬, 자바스크립트, Go 언어 등 많은 프로그래밍 언어에서 가비지 컬렉션이 기본으로 내장되어 있으며, 브라우저 역시 자체적으로 구현된 가비지 컬렉션이 있다.

 

하지만 이렇게 천사같아 보이는 GC에도 아래와 같은 단점이 존재한다.

  1. 자동으로 처리해준다지만, 정확히 언제 메모리가 해제되는지 알기 어려워 메모리를 능동적으로 제어하기 어렵다.
  2. 가비지 컬렉션이 동작할 때, GC 쓰레드를 제외한 나머지 쓰레드가 모두 멈추기 때문에 오버헤드가 발생할 수 있다.

2번 문제를 전문 용어로 *Stop-The-World*라고 한다.

(GC가 동작할 때는 세상이 중지한다? ㅋㅋㅋ)

GC를 수행하기 위해 JVM이 프로그램 실행을 멈추는 현상을 의미하고, 서비스 이용에 차질이 생길 수 있어 이 시간을 최소화 시키는 것이 쟁점이다.

 


GC의 대상 객체

GC는 어떤 객체를 청소해야된다고 판단해서 스스로 지워버리는걸까?

GC는 특정 객체가 garbage인지 아닌지 판단하기 위해서 도달성(Reachability)이라는 개념을 사용한다.

 

Heap 영역의 객체에 레퍼런스가 있다면 *Reachable*로 구분되고, 객체에 유효한 레퍼런스가 없다면 *Unreachable*로 구분해 청소해버린다.

 

 


GC의 청소 방식

위에서는 어떻게 Unreachable한 객체를 찾아내는지 간단히 알아봤다.

그럼 찾아낸 Unreachable한 객체는 어떻게 청소할까? 바로 *Mark And Sweep* 방식을 이용한다.

 

Mark And Sweep 이란 다양한 GC에서 사용되는 아주 기초적인 청소 과정이다.

 

가비지 컬렉션이 될 대상 객체를 식별(Mark)하고 제거(Sweep)하며 객체가 제거되어 파편화된 메모리 영역을 앞에서부터 채워나가는 작업(Compaction)을 수행한다.

-> Compaction 과정은 GC의 종류에 따라 사용하지 않는 경우도 있다.

 


GC의 동작 원리

- Heap 영역의 구조

먼저 Heap 영역을 조금 자세히 살펴보자.

JVM의 Heap 영역은 동적으로 참조형 데이터가 저장되는 공간으로, *가비지 컬렉션의 대상*이 되는 공간이다.

Heap 영역은 처음 설계될 때 부터 다음의 2가지 전제(Weak Generational Hypothesis)로 설계되었다.

  1. 대부분의 객체는 금방 접근 불가능한 상태(Unreachable)가 된다.
  2. 오래된 객체에서 새로운 객체로의 참조는 아주 적게 존재한다.

즉, 객체는 대부분 일회성이며 메모리에 오래 남아있는 경우는 드물다는 것이다.

이러한 특성을 이용해 JVM 개발자들은 효율적인 메모리 관리를 위해 객체의 생존 기간에 따라 Young과 Old, 총 두 가지 영역으로 설계했다. 초기에는 Perm 영역도 존재하였지만, Java8부터 제거되었다.

 

Young 영역 (Young Generation)

  • 새롭게 생성된 객체가 할당되는 영역
  • 대부분의 객체가 금방 Unreachable 상태가 되므로 많은 객체가 생성되었다가 사라진다.
  • Young 영역에 대한 GC를 *Minor GC*라고 부른다.

Old 영역 (Old Generation)

  • Young 영역에서 Reachable 상태를 유지하여 살아남은 객체가 이동하는 영역
  • Young 영역보다 크게 할당되며, GC는 적게 발생한다.
  • Old 영역에 대한 GC를 Major GC라고 부른다.

 

- Minor GC

Young 영역은 더욱 효율적인 GC를 위해 3가지 영역으로 다시 나눠진다.

  • Eden
    • new를 통해 새로 생성된 객체가 위치
    • 정기적인 GC 후 살아남은 객체들은 Survivor 영역으로 보냄
  • Survivor 0, 1
    • 최소 한 번의 GC 이상을 살아남은 객체가 존재하는 영역
    • 이 구역에는 특별한 규칙이 있는데, 두 구역중 하나는 반드시 비워져있어야 한다.

이제 Young 영역에서 발생하는 Minor GC에 대해 알아보자. 아래는 동작 과정을 서술한 것이다.

  1. 모든 객체는 처음에 Eden에 위치하게 된다.
  2. 객체가 계속 생성되어 Eden 영역이 꽉차면 Minor GC가 발생한다.
  3. Mark 동작을 통해 reachable 한 객체를 탐색한다.
  4. Eden 영역에서 살아남은 객체는 1개의 Survivor 영역으로 이동한다.
  5. Eden 영역에서 사용되지 않는 객체의 메모리를 해제한다. (sweep)
  6. 살아남은 모든 객체들은 age 값이 1씩 증가한다 -> Object Header에 기록된다.
  7. 또 다시 Eden 영역이 꽉차면 Minor GC가 발생한다.
  8. .... 반복한 뒤 살아남은 모든 객체들은 반대편 Survivor 영역으로 이동한다.
  9. 위 과정을 반복한다.

 

- Major GC

Major GC는 Old Generation 영역의 메모리가 부족해지면 발생한다.

동작 과정은 아래와 같다.

  1. Survivor 영역에 존재한는 객체의 age가 특정 임계값에 도달한다면 Old Generation 영역으로 이동한다. (Promotion)
  2. 이를 반복하다보면 Old 영역의 메모리가 꽉 찬 순간이 온다. 이 때 Major GC가 발생한다.

Major GC는 Old 영역의 데이터가 가득 차면 GC를 실행하는 단순한 방식이다.

Major GC는 상대적으로 큰 공간을 가진 Old 영역에 대해서 GC가 진행되기 때문에 메모리 해제에 많은 시간이 걸리게 된다.

 

통상적으로 Young 영역은 일반적으로 0.5초에서 1초 사이에 끝나는데, 이 정도 숫자는 애플리케이션에 큰 영향을 주진 않는다.

하지만 Major GC는 Minor GC보다 *10배 이상의 시간*을 사용한다.

 

여기서 Stop-The-World 문제가 발생하는 것이다.

Major GC가 자주 발생하면 Thread가 멈추고, Mark And Sweep 작업을 계속 해야해서 CPU에 부하를 주기 때문에 멈추거나 버벅이는 현상이 일어난다.

 

따라서 자바 개발진들은 끊임 없이 GC 알고리즘을 발전시켜왔다.

 


다양한 GC 알고리즘

- Serial GC

  • 서버의 CPU 코어가 1개일 때 사용하기 위해 개발된 *가장 단순한 GC*
  • GC를 처리하는 쓰레드가 1개여서 stop-thw-world 시간이 가장 길다.
  • Minor GC에는 Mark-Sweep, Major GC에는 Mark-Sweep-Compact를 사용한다.
  • 실무에선 보통 사용하지 않는다.

 

- Parallel GC

  • Java8의 디폴트 GC
  • Serial GC와 기본적인 알고리즘은 같지만, *Young 영역의 Minor GC를 멀티 쓰레드로 수행*한다.

 

- Parallel Old GC

  • Parallel GC를 개선한 버전
  • Young 영역 뿐 아니라 Old 영역에서도 멀티 쓰레드로 GC를 수행
  • 새로운 가비지 컬렉션 청소 방식인 Mark-Summary-Compact 방식을 이용

 

- CMS GC (Concurrent Mark Sweep)

  • 어플리케이션의 쓰레드와 GC 쓰레드가 동시에 실행되어 stop-the-world의 시간을 최대한 줄이기 위해 고안된 GC
  • GC 과정이 매우 복잡해짐
  • 메모리 파편화 문제
  • 결국 Java14에서는 *사용이 중지됨*

 

- G1 GC (Garbage First)

  • CMS GC를 대체하기위해 등장한 GC, Java9+ 버전의 디폴트 GC로 지정
  • Heap이 너무 작을 경우 미사용 권장
  • 기존 GC 알고리즘은 Heap을 물리적으로 고정된 Young / Old 영역으로 나누어 사용했지만, 
    G1 GC는 이런 개념을 완전히 박살내고 *Region*이라는 개념을 새로 도입
    전체 Heap 영역을 체스판 같이 분할하여 상황에 따라 Eden, Survivor, Old 등 역할을 동적으로 부여
  • garbage로 가득찬 영역을 빠르게 회수하여 빈 공간을 확보하므로, 결국 GC 빈도가 줄어드는 효과를 얻게되는 원리

Region

 

- Shenandoah GC

  • Java12에 릴리즈
  • RedHat에서 개발한 GC
  • 기존의 CMS가 가진 단편화 문제, G1이 가진 pause의 이슈를 해결
  • 강력한 Concurrency와 가벼운 GC 로직으로 Heap 사이즈에 영향을 받지 않고 일정한 pause 시간이 소요되는 것이 특징

 

- ZGC

  • Java15에 릴리즈
  • 대량의 메모리(테라바이트 급)을 low-latency로 잘 처리하기 위해 디자인된 GC
  • G1의 Region처럼 ZGC는 *ZPage*라는 영역을 사용한다. Region은 크기가 고정이지만 ZPage는 동적으로 운영된다.
    (큰 객체가 들어오면 2^ 단위로 영역을 구성해서 처리)
  • ZGC가 내세우는 최대 장점 중 하나는 Heap 크기가 증가해도 stop-the-world 시간이 *절대 10ms를 넘지 않는다는 것*이다.