본문 바로가기
Language/Java

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

by 기몬식 2023. 8. 21.

Garbage Collection

GC는 자바의 메모리 관리를 자동화하는 메커니즘으로 명시적으로 메모리를 할당하거나 해제하지 않아도 동적으로 사용하지 않는 객체들을 인식하고 제거하는 프로세스입니다.
GC의 자동 메모리 관리로 인해 프로그래머들은 메모리 누수 문제, 메모리 관리 등과 같은 문제에서 벗어나 오로지 개발에만 집중 할 수 있게 되었습니다.

하지만 GC는 일시적으로 메모리에 대한 가비지를 수집할 때까지 GC 스레드를 제외한 모든 스레드를 중지하는 Stop the world 이벤트를 진행하는데 이는 오버헤드가 발생돼 프로그램의 응답성이 떨어지는 문제점이 있습니다.
GC가 오히려 자주 실행되면 프로그램의 실행 시간이 증가되어 성능상의 이슈가 발생합니다.
따라서 실시간으로 제어하는 것이 중요한 프로그램이나 GC가 예상치 못한 타이밍에 발생해 문제를 겪을 수 있는 프로그램같은 경우는 프로그래머들이 직접 메모리를 제어, 관리하는 것이 더 바람직할 수도 있습니다.

동작 원리

대상

GC 동작 대상은 더이상 사용되지 않는 객체들을 대상으로합니다.
이러한 객체들을 가비지(Garbage)라고 하며 GC의 주요 목표는 이러한 가비지들을 탐지하고 메모리에서 해제하는 것입니다.

GC는 객체에 대한 도달가능성(Reachability) 을 판단하여 가바지 컬렉션 대상을 결정하는데 사용합니다.
먼저 자바 프로그램은 객체들 사이의 참조(Reference)를 통해 상호 작용합니다.
이 중 객체에 참조가 남아있다면 도달 가능한 객체 (Reachable Objects) 로 구분되어 GC Root로부터 참조 체인을 통해 도달할 수 있는 객체로 구분됩니다.
반대로 참조가 없다면 도달 불가능한 객체 (Unreachable Objects) 로 구분되며 GC Root로부터 참조 체인을 통해 도달할 수 없는 객체로 구분됩니다.

출처

GC는 각 영역의 GC Root에서 인스턴스 필드와 다른 객체에 대한 참조를 따라 메모리의 전체 그래프를 순회합니다.
이때 GC가 방문하는 모든 객체는 활성 상태로 표시되며 GC Root와 연결되지 못한 모든 객체들은 비활성화 상태가 됩니다.
즉 활성 상태가 된 객체는 도달 가능한 객체로 분류되며 비활성화 상태의 객체는 도달 불가능한 객체로 구분됩니다.

위와 같이 GC는 주기적으로 GC Root로부터 객체에게 도달 가능한지 여부를 판단하여 도달 가능한 객체들은 해제되지 않은채 유지되며 도달 불가능한 객체들은 GC의 대상이 되어 메모리에서 해제됩니다.

GC Root

GC Root란 GC의 출발점이 되는 객체를 의미합니다.
이 객체들은 다른 객체들을 참조하거나 도달 가능한 객체들로부터의 첫 번째 진입점을 제공하여 GC의 시작점으로 동작합니다.
GC Root로 인식되는 객체들은 다음과 같습니다.

  1. 스택프레임(Stack Frame): 스택 프레임에 있는 객체들은 GC Root로 인식됩니다. 스택은 현재 실행 중인 메소드의 상태를 나타내며 실행중인 메소드가 참조하는 객체들은 GC Root로부터 도달 가능한 상태라고 간주합니다.
  2. 정적(Static) 변수: 프로그램의 시작부터 끝까지 존재하기때문에 프로그램 수명 주기 동안 도달 가능한 상태입니다.
  3. JNI(Java Native Interface)에서 생성된 객체: 네이티브 코드에서 참조하고 사용되므로 GC가 이러한 객체들을 인식하려면 JNI를 통해 생성된 객체들을 GC Root로 유지해야 합니다.
  4. 현재 실행 중인 스레드: 실행 중인 스레드는 객체를 참조할 수 있는 상태를 유지하기 때문에 GC Root로 인식됩니다.

동작

GC는 참조 체인을 통해 어떤 객체가 도달 가능한지, 불가능한지 식별합니다.
그 후 도달 불가능한 객체들의 메모리를 다음과 같이 해제합니다.

Mark and Sweep

Mark and Sweep은 GC 알고리즘 중 하나로 동적으로 할당된 메모리 중에서 더 이상 사용되지 않는 객체(도달 불가능한 객체 또는 가비지)를 탐지하여 해제하는 작업을 수행하는 방법입니다.
먼저 GC 대상 객체를 식별(Mark)하고 제거(Sweep) 후 제거 되어 파편화된 메모리 영역을 앞에서부터 채우게(Compaction) 됩니다.

  1. Mark: GC Root로부터 참조 체인을 통해 도달 가능한 객체를 마킹하며 Mark 단계 이후 마킹되지 않은 객체는 가비지로 간주됩니다.
  2. Sweep: 마킹되지 않은 모든 객체들을 해제하여 메모리를 회수합니다. 해제된 메모리는 다시 사용 가능한 메모리로 표시됩니다.
  3. Compact: Mark와 Sweep 단계를 통해 해제된 객체들로 인해 파편화된 메모리를 한쪽으로 모으고 사용 중인 객체들 사이의 빈 공간을 줄여서 메모리 파편화를 해소합니다. Compact 단계 실행 중에 객체들의 상대적인 위치가 변경됨으로 해당 객체들의 포인터도 적절히 업데이트되어야 합니다.

출처

과정

Minor GC

Minor GC는 Generational Garbage Collection 중 하나로 Young Generation에서 발생하는 GC를 의미합니다.
대부분의 객체는 생성 후 일시적으로 사용되다가 더 이상 참조되지 않게 되는데 이러한 객체들을 관리하기 위한 영역이 Young Generation입니다.
Young Generation은 일반적으로 GC가 더 빈번하게 일어나는데 Old Generation의 크기보다 상대적으로 작기때문에 메모리 상의 객체를 찾아 제거하는데 상대적으로 적은 시간이 소요됩니다.

먼저 Eden 영역은 모든 객체가 최초 생성 시 저장되는 영역으로 첫번째 이미지와 같이 모든 객체들이 메모리를 할당받게 됩니다.

그 후 Eden 영역이 가득차게 되면 Minor GC가 발생하여 참조가 없는 객체들은 메모리가 해제되며 참조 중인 객체는 Survivor Spaces 중 Survivor 0(S0, From Area) 영억으로 이동합니다.

또 다시 Eden 영역이 가득 차게 되면 Minor GC가 발생합니다.

그리고 살아남은 모든 객체들의 Age 값을 1씩 증가시킵니다.

또 다시 Eden 영역이 가득 차게 되면 Minor GC가 발생합니다.

살아남은 모든 객체들을 Survivor 1(S1, To Area)로 이동시킵니다.

그 후 Minor GC 주기 동안 살아남은 객체의 Age를 1씩 증가시킵니다.

이런 식으로 Minor GC는 주기마다 살아남은 객체들의 Age를 증가시키며 S0, S1 영역을 번갈아가며 객체들을 저장합니다.

Age

GC에서 Age 값은 객체의 생성 이후 얼마나 오래 살아 남았는지를 나타내는 값입니다.
GC는 Age 값이 특정 임계값(Threshold) 이상이 되면 이 객체를 오래된 객체로 판단해 Old Generation 영역으로 이동(Promotion) 여부를 결정합니다.
Age는 Object Header에 기록되며 6 bit 용량이기 때문에 일반적인 HotSpot JVM의 경우 이 age의 기본 임계값을 31로 설정합니다.

Major GC

Major GC는 Generational Garbage Collection 중 하나로 Old Generation에서 발생하는 GC를 의미합니다.
Full GC라고도 불리며 Minor GC 과정에서 제거되지 않고 Age 임계값이 가득 차게되어 Old Generation으로 승격(Promotion)한 객체들을 대상으로 동작합니다.

객체의 Age 값이 임계값에 도달한 객체를 식별합니다.

임계값에 도달한 객체는 Old Generation으로 이동됩니다.
즉 Promotion(승격이) 일어납니다.

위와 같은 과정이 반복되어 Old Generation 영역이 가득 차게(메모리가 부족하게)되면 Major GC가 동작합니다.

Major GC는 위와 같이 단순 Old Generation의 영역이 가득 차게 되면 실행 시킵니다.
하지만 Young Generation보다 더 큰 영역의 모든 객체를 검사하여 참조되지 않는 객체를 한 번에 삭제하기 때문에 상대적으로 오랜 시간이 소요됩니다.
때문에 Minor GC에 비해 오랜 시간이 소요되고 애플리케이션에 부하가 크게 갑니다.

위와 같이 Major GC는 다소 무거운 작업을 진행하게 됩니다.
Major GC가 발생하면 스레드가 멈추고 Mark and Sweep 작업을 진행하며 Stop the world 이벤트가 발생합니다.
이는 애플리케이션의 일시적인 중단을 초래할 수 있으며 CPU에 많은 부하를 주게됩니다.

알고리즘

출처

GC를 수행하게 되면 Stop the world 이벤트가 발생되고 이는 애플리케이션이 가동되는데 여러 문제를 발생시켰습니다.
또한 각 하드웨어와 자바 언어의 발달로 인해 허용 가능한 Heap 사이즈가 커지게 되면서 애플리케이션의 지연 현상이 빈번하게 발생했으며 이는 곧 최적화의 필요성을 느끼게 되었습니다.
따라서 위와 같은 이유로 자바 개발자들은 끊임 없이 GC 알고리즘을 발전 시켜왔으며 다음과 같은 알고리즘이 있습니다.

Serial GC

출처

Serial GC는 단일 스레드에서 작동하는 GC 알고리즘입니다.
Young Generation과 Old Generation 모두 단일 스레드로 가비지 컬렉션을 수행하며 Minor GC 에는 Mark-Sweep을 사용하고 Major GC에는 Mark-Sweep-Compact를 사용합니다.
단일 스레드에서 GC를 수행하기 때문에 CPU 사용률이 높아지고 애플리케이션이 일시적으로 중단될 위험이 높기때문에 실제 운영 환경에서는 성능과 응답성 측면에서 다른 GC 알고리즘이 더 적합합니다.
작은 규모의 애플리케이션 또는 단일 프로세서 시스템에서 사용될 수 있습니다.

Serial GC Command

java -XX:+UseSerialGC -jar ${applicationName}.java

Parallel GC

출처

Paralle GC는 대량의 데이터를 처리하는 애플리케이션 및 멀티코어 프로세서 시스템에서 사용될 수 있으며 Java 8의 기본 GC 알고리즘입니다.
기본적으로 Serial GC와 기본적인 알고리즘은 같지만 Young Generation에서 Minor GC를 멀티 스레드를 사용하여 병렬로 수행하지만 Old Generation은 여전히 단일 스레드로 작동합니다.
Serial GC에 비해 Stop the world 시간이 감소됐습니다.

Parallel GC Command

java -XX:+UseParallelGC -jar ${applicationName}.java 
# -XX:ParallelGCThreads=N : 사용할 쓰레드의 수

CMS GC (Concurrent Mark Sweep)

출처

CMS GC는 애플리케이션과 함꼐 실행되는 GC로 주로 응답성을 개선하는 데 중점을 둔 Old Generation에서만 사용되는 알고리즘입니다.
CMS GC는 전체적인 애플리케이션 중단 시간(Stop the world)을 최소화하려는 목표로 고안되었으며 GC 과정이 여러 단계로 구분되고 매우 복잡하기 때문에 다른 GC 대비 CPU 사용량이 높습니다.

Initial Mark, Concurrent Mark, Remark, Concurrent Sweeping 크게 4단계로 동작하며 각 단계별 주요 특징은 다음과 같습니다.

  1. Initial Mark: 스레드를 멈추고(Stop the world) GC Root 객체들과 참조하는 객체들을 표시합니다.
  2. Concurrent Mark: 애플리케이션 실행과 병렬로 진행되며 Initial Mark에서 표시된 객체와 간접 참조되는 객체들을 표시합니다. 애플리케이션 스레드와 별도의 GC 스레드가 동시에 작동합니다.
  3. Remark: 애플리케이션 실행을 일시 중단하고 이전 Concurrent Mark에서 변경된 객체들을 다시 확인하고 표시합니다. 짧은 중단 시간이 발생하지만 표시되지 않은 객체들을 정확히 식별하기위한 작업입니다.
  4. Concurrent Sweeping: 가비지 객체를 GC 과정으로 Stop the world 없이 애플리케이션 실행과 동시에 진행됩니다.

위와 같이 애플리케이션 중단 시간을 최소화하여 GC를 동작하기 위해 고안된 알고리즘이지만 GC와 애플리케이션 실행을 동시에 처리하면서 메모리 파편화 문제가 심하게 대두되었고 처리량(throughput) 측면에서는 다른 알고리즘들에 비해 성능이 떨어지는 이유로 Java 9부터 deprecated 되었고 Java 14에서 삭제되었습니다.

CMS GC Command

java -XX:+UseConcMarkSweepGC -jar ${applicationName}.java

G1 GC (Garbage First)

출처

CMS GC를 대체하기 위해 Java 7버전에서 release된 GC 알고리즘으로 Java 9 이상의 버전에서는 기본 GC 알고리즘으로 채택되었습니다.
G1 GC는 큰 Heap 사이즈를 가진 대규모 애플리케이션과 멀티 프로세서 시스템에서 동작하기 위해 설계되었습니다.

기존의 GC 알고리즘에서는 Heap 영역을 물리적으로 고정된 Young & Old 영역으로 나누어 사용하였지만 G1 GC는 Region이라는 새로운 개념을 도입하여 Heap 영역을 각각 분할하고 동적으로 각 Region에 역활을 부여합니다.
G1 GC는 가장 많은 가비지가 있는 Region을 우선적으로 처리하여 GC 빈도를 줄이고 애플리케이션 중단 시간을 최소화하고 더 잘 분산된 GC를 제공하여 응답성을 향상시킵니다.

G1 GC Command

java -XX:+UseG1GC -jar ${applicationName}.java

Shenandoah GC

Shenandoah GC는 RedHat에서 개발한 GC로 대규모 애플리케이션에서의 응답성을 높이기 위해 개발된 GC 알고리즘입니다.
작은 GC를 여러번 동작해 즉 GC가 CPU를 더 사용하는 대신 Pause(Stop the world) 시간을 줄이는 목적으로 개발되었습니다.
Low Pause GC라고도 불리며 Java 12부터 실험적으로 도입되었으며 Java 15부터는 실제 운영 환경에서 사용 가능한 기능으로 제공되었습니다.

다음은 Low Pause GC의 동작 단계를 설명한 내용입니다.

  1. Initial Mark: 이때 애플리케이션의 모든 스레드가 잠시 짧은 시간 동안 멈추며 GC Root와 직접 참조하는 객체를 표시합니다.
  2. Concurrent Mark: 애플리케이션의 실행과 병렬로 수행되는 단계로 Initial Mark 단계 이후에 추가적인 객체들을 표시하고 앞서 표시한 객체들과 간접 참조되는 객체들을 찾아내어 가비지 객체를 식별합니다.
  3. Final Mark: Concurrent Mark 단계에서 놓친 가비지 객체들을 추가로 표시하며 이동 가능한 Region을 파악하여 다음 단계를 위한 Runtime을 준비합니다. Final Mark 단계에서 두번째 Pause를 발생시킵니다.
  4. Concurrent Sweep: 애플리케이션의 실행과 동시에 객체들이 해제되므로 중단 시간은 최소화됩니다.

기존 CMS GC가 가진 메모리 파편화, G1이 가진 Pause의 이슈를 해결한 알고리즘으로 강력한 Concurrency와 가벼운 GC 알고리즘으로 Heap 사이즈에 영향을 받지 않고 일정한 Pause 시간을 줄이는 알고리즘입니다.

Shenandoah GC Command

java -XX:+UseShenandoahGC -jar ${applicationName}.java

ZGC (Z Garbage Collector)

출처

ZGC는 G1 GC처럼 전체 Heap 공간을 ZPage라는 영역으로 분할하여 GC를 수행합니다.
G1 GC의 Region의 크기는 고정인데 반해 ZPage는 2mb 배수로 동적으로 할당되며 사용되는 주요 알고리즘은 다음이 있습니다.
ZGC는 ava 11부터 실험적으로 도입되었으며 Java 15부터는 실제 운영 환경에서 사용 가능한 GC로 대규모 애플리케이션과 대량의 메모리(8MB ~ 16TB)를 가진 시스템에서 낮은 중단 시간과 높은 성능을 제공하기 위해 설계되었습니다.
ZGC의 특징은 Low Latency와 Scalable로 나타낼 수 있는데 이는 GC Pause 시간이 10ms 미만이며 Heap이나 라이브셋의 사이즈가 커져도 Pause 시간이 늘어나지 않도록 Pause 시간을 제어함과 높은 성능입니다.
Pause 시간은 10ms 미만, G1 대비 성능은 15% 향상되었다고 발표했습니다.

ZGC는 GC 과정 중에 총 3번의 중단 시간이 발생하며 GC 동작 순서는 다음과 같습니다.

  1. Pause
  2. Mark Start: GC Root를 Mark합니다.
  3. Concurrent Mark/Remap: GC Root로부터 도달 가능한 모든 객체를 Mark합니다.
  4. Pause
  5. Mark End: 새롭게 추가된 객체들에 대해서 Mark합니다.
  6. Concurrent Pereare for Reloc: 재배치 하려는 영역을 찾아 RelocationSet에 배치합니다.
  7. Pause
  8. Relocate Start: 모든 GC Root의 재배치를 진행하고 업데이트합니다.

Colored Pointers

Colored Pointers는 객체가 실제 할당된 메모리에서 64 bit를 활용하여 저정한 객체의 GC 메타데이터를 표시합니다.

출처

  • Finalizable: finalizer을 통해서만 참조되는 Object로 가비지 객체입니다.
  • Remapped: 재배치 여부를 판단하는 Mark입니다.
  • Marked 1: Live Object
  • Marked 0: Live Object

Load Barriers

출처

Load Barriers는 Concurrency GC 동안 애플리케이션 실행 중 메모리 접근을 안전하게 수행하기 위해 사용되는 알고리즘입니다.
Load Barriers는 JIT 컴파일러가 애플리케이션의 코드에 삽입하며 해당 객체를 참조할 때 해당 객체 주소의 Colored Point(GC 메타데이타)가 Bad Color인지 확인합니다.
객체에 대한 접근 시에 추가적인 안전성 검사를 수행하며 만약 Bad Color라면 객체의 상황에 따라 Mark & Relocate & Remapping를 진행합니다.
Heap 참조 시 항상 Load Barriers를 거쳐야 하므로 약간의 성능 저하가 발생하지만 다른 GC 알고리즘과 비교했을 때 전체적으로 미치는 성능의 영향은 미미합니다.

ZGC Command

java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -jar ${applicationName}.java

오탈자 및 오류 내용을 댓글 또는 메일로 알려주시면, 검토 후 조치하겠습니다.