[JVM] JVM의 GC

Hyunjun Kim·2025년 4월 19일
0

Data_Engineering

목록 보기
43/153

3. JVM의 GC

3.1 GC 과정

앞에서는 JVM의 메모리 구조에 대해서 배웠었다.
이번엔 JVM의 GC과정에 대해 배우고 그 메모리 구조를 어떻게 Garbage Collection에서 활용하는지 배워보자


이미지 출처 : https://nays111.tistory.com/110

3.1.1 Minor GC

  1. 객체 생성
  • 새로운 객체는 Young Generation의 Eden 영역에 생성된다.
  1. Eden 영역이 가득 차면 Minor GC 발생
  • Eden이 가득 차면 Minor GC가 발생한다.
  • 이때 Eden에 있는 객체들은 GC 대상으로 마킹되며, 살아남은 객체만 추려낸다.
  1. Eden → Survivor 복사
  • Eden에서 살아남은 객체들은 Survivor 영역 중 비어 있는 쪽(to)으로 memory copy된다.
  • 사용되지 않는 객체는 제거된다.
  1. Survivor(from) → Survivor(to) 또는 Old Gen 복사
  • 이전 GC에서 살아남아 현재 사용 중인 Survivor 영역(from)에 있는 객체도 GC 대상이 된다.
  • 이 중에서 충분히 오래 살아남은 객체(age threshold 도달)는 Old Generation으로 이동하고,
    나머지는 Eden에서 이동한 객체들과 함께 Survivor의 to 영역으로 이동한다.
  1. Eden, Survivor(from) 정리
  • GC가 끝나면 Eden과 Survivor(from)는 모두 비워진다.
  • Survivor(to)는 다음 Minor GC에서 from 영역으로 역할이 전환된다.
  • 이후 GC가 발생하면 to ↔ from 역할이 계속 논리적으로 교체된다.

성능 고려 사항

  • Eden → Survivor, Survivor(from) → Survivor(to), Survivor → Old로 객체가 이동할 때는 memory copy가 발생한다. 이 과정은 Minor GC 시 수행되며, GC 비용에 직접적인 영향을 준다.
  • Survivor 0과 1은 물리적으로 분리된 공간이며, GC가 발생할 때 살아남은 객체는 from 영역에서 to 영역으로 복사된다. 이후 GC가 끝난 뒤에는 논리적인 역할 전환만 발생하며, 이는 포인터 교체 수준이므로 복사 비용이 없다.
  • 객체의 라이프사이클이 짧을수록 Old Generation으로 이동하는 객체 수가 줄어들어, 전체 GC 부하를 낮출 수 있다.
  • Minor GC는 Young Generation 대상의 비교적 빠른 GC지만, 객체가 자주 Old 영역으로 승격되면 Old 영역의 Full GC 트리거 가능성이 높아져 전체 GC 성능에 부정적 영향을 줄 수 있다.

3.1.2 Major GC

Major GC는 Old Gen 영역을 정리하기 위한 GC이다.
JDK 버전과 GC 알고리즘에 따라서 정확히 언제 수행하는 지는 달라진다.

3.1.3 Full GC

Minor GC 와 Major GC 가 동시에 이루어질 때. YoungGen, Old gen 모두 가용 영역이 부족할 때 생긴다.
Full GC 때 STW(stop-the-world)가 일어난다.

  • STW에는 JVM 프로그램의 전체가 아무런 일도 할 수 없으므로, STW 이벤트의 발생 빈도가 적도록, STW시간이 적게 걸리도록 지속적으로 모니터링하고 프로그램이나 JVM 파라미터를 수정해야한다.

CMS(concurrent mark and swap), G1 GC의 경우 이 Full GC를 최소화 하도록 설계되었다.

다만, 알고리즘이 모든 것을 보장해주지 않는다.
프로그래밍 된 코드가 객체의 주기를 짧게 가져가고 오래 살아남는 객체를 최소화 한다면, Full GC가 최소한으로 일어나도록 할 수 있다. 코드가 더욱 중요하다.

3.1.4 GC 동작 순서

1) Graph


출처 : https://imasoftwareengineer.tistory.com/103

  • GC Root에서 시작해서 참조하는 객체를 찾고, 또 그 객체가 참조하는 객체를 찾아가며 Mark 한다.
  • Mark 되지 않은 객체(Island)는 접근할 수 없는 객체 (Unreachable Object)로 판단하고 제거(Sweep)한다.

2) GC Root


이미지 출처 : https://kimss1502.github.io/java/%EC%9E%90%EB%B0%94%EC%9D%98-Reference-%ED%98%95%ED%83%9C%EC%99%80-GC/

GC Root 될 수 있는 종류 세 가지.

  • JVM stack 의 변수
    스택에 있는 변수인데 스택에서 가장 먼저 시작하는 클래스나 변수

  • Method Area 의 static 데이터
    Static 변수나 클래스 정보는 Method Area에 저장된다. 이 영역은 클래스 로딩 시부터 종료 시까지 JVM에 의해 유지되기 때문에, static 데이터 역시 GC Root가 될 수 있다. 이 static 데이터가 다른 객체를 참조하면, GC는 해당 static 데이터를 기준으로 참조 체인을 따라가며 어떤 객체를 살아 있는 것으로 판단할지 결정한다.

  • JNI 에서 생성된 객체

3) Mark, Sweep, Compact

  • Mark: 접근 가능한(reachable) 객체에 표시
  • Sweep: Mark 안된 객체들(unreachable)을 제거
  • Compact: Sweep에 의해 메모리가 삭제되면서 발생하는 Fragmentation 을 정리하기 위해서 (연속된 메모리 공간을 갖게 하기위해서) 메모리를 재할당(과정에서 copy 일어남)하여 정리.

정리.
GC할 때 필요한 요소들
마킹하는 과정
어던 게 루트가 되는지
루트로부터 연결되지 않은 친구는 섬이 된다 -> sweep의 대상이 된다.
sweep후엔 메모리프레그멘테이션을 해결하는 게 중요하다.
그 중 하나가 컴팩트 하는 과정이 있다.

3.2 Serial GC

GC알고리즘을 하나씩 알아보자.

3.2.1 Serial GC 동작

화살표가 여러 개의 스레드라고 보면 됨.
Program이 여러개 수행 중인데 전부 멈춰. 하고 GC스레드 나혼자만 돈다.
GC 스레드는 하나뿐이고, 혼자서 여기저기 다 다니면서 마킹하고 sweep하고 compact하는 과정을 진행.
GC 끝났어. 다 이제 돌아! 하면 다른 스레드들이 다시 돌게 되는 구조.
그래서 GC스레드 하나만 돌기 때문에 Serial GC라고 부름.

3.2.2 특징

  • 싱글 스레드로 작동한다.
  • 느리고, STW가 길다.
  • Mark & Sweep & Compact 를 사용
  • 거의 사용하지 않는다. (STW가 기니까.)

3.3 Parallel GC

3.3.1 Parallel GC동작

이미지 출처 : https://memostack.tistory.com/229

Parallel GC는 "모두 멈춰!" 한 다음에, 가용한 여러 개의 스레드를 동시에 활용해 GC를 빠르게 수행하는 방식이다.
예를 들어, 1번 스레드는 루트1번부터, 2번 스레드는 루트2번부터 시작해 마킹 작업을 병렬로 나눠서 처리한다.

이후 살아있지 않은 객체를 각 스레드가 자신이 맡은 메모리 영역에서 나눠서 삭제하고, 압축이 필요한 경우에도 서로 협력해 각자 맡은 영역을 정리한다.

즉, 마킹 → 삭제 → 압축의 전 과정이 여러 스레드에 의해 동시에 분담되고 수행되는 구조이며, 이 병렬 처리를 통해 GC 시간을 줄일 수 있다.

3.3.2 특징

  • Young Gen의 GC를 멀티스레드로 GC한다.
  • Serial 에 비해 STW가 짧다.
  • ParallelOldGC를 사용하면 old 영역까지 멀티스레드로 할 수 있다.

3.3.3 설정

Java 7,8 의 기본 GC이다.

  • -XX:+UseParallelGC

ParallelOldGC

  • -XX:+UseParallelOldGC
  • -XX:+ParallelGCThreads=n
    • n 자리로 스레드 수 지정 가능하다. (내 컴퓨터 가용 코어 수 알면 좋다)

3.4 CMS GC

3.4.1 CMS GC 동작


이미지 출처 : https://www.oracle.com/technetwork/java/javase/memorymanagement-whitepaper-150215.pdf

CMS(Concurrent Mark-Sweep) GC는 Old 영역에서 수행되는 가비지 컬렉터로, Stop-The-World(STW) 시간을 최소화하기 위해 설계되었다.
Mark-Sweep 방식으로 동작하며, Mark 단계를 총 4단계로 나누어 수행한다.

3.4.2 CMS GC 동작 4단계

1단계: Initial Mark (STW)

GC Root가 직접 참조하고 있는 객체만 우선적으로 표시(mark)한다.
빠르게 Root 객체만 식별하는 작업이므로 짧은 시간 동안만 애플리케이션을 멈춘다(STW).

2단계: Concurrent Mark (No STW)

Initial Mark에서 찾은 Root 객체를 기준으로, 그와 연결된 객체들을 따라가며 전체 힙에서 참조 가능한 객체들을 표시한다.
이 작업은 GC 스레드가 백그라운드에서 수행하며, 이때도 애플리케이션 스레드는 동시에 실행되므로 STW는 발생하지 않는다.

3단계: Remark (STW)

Concurrent Mark 도중 애플리케이션이 새롭게 객체를 생성하거나 참조를 바꾸는 일이 생길 수 있다.
이 단계에서는 그 변경 사항을 다시 확인하여 마킹 정보를 보정한다.
정확성을 위해 애플리케이션을 다시 멈추지만, 대부분의 마킹 작업이 이미 끝난 상태이기 때문에 STW 시간은 짧다.

4단계: Concurrent Sweep (No STW)

마킹되지 않은 객체(즉, 더 이상 사용되지 않는 객체)를 제거(sweep)한다.
이 과정 또한 GC 스레드가 백그라운드에서 실행되며, 애플리케이션은 계속 동작한다.

3.4.3 CMS GC의 장점

  • Multi Thread로 수행하고 Stop the World를 적게 가져가니까 프로그램 장치가 멈추지 않아 가장 Side Effect가 적다.
  • 앞에 있던 Parallel 이나 Serial 보다 빠르다.

3.4.4 CMS GC의 단점

  • 다른 GC보다 CPU와 메모리를 더 많이 사용한다.
  • Compaction 단계가 기본 제공되지 않아서 Old 영역에 Fragmentation이 좀 존재할 수 있다.

3.4.5 설정

명시적인 지정 방법

  • -XX:+UseConcMarkSweepGC
  • -XX:+CMSInitiatingOccupancyFraction=n
    • Old 영역의 %를 지정. 기본값 68
  • -XX:+CMSIncrementalMode
    • 점진적인 방식으로 Young GC를 더 작은 단위로 나눈다.

3.5 G1 GC

CMS의 단점인 Fragmentation을 줄이고, GC의 부하를 낮추기 위해 고안된 방법이다.

G1 GC 운영 가이드
G1 GC를 사용할 때, GC 시간이 얼마나 걸리는 것이 적절한지 판단하기 어려울 수 있다.
튜닝이 필요한 상황인지, 코드 리팩토링이 필요한지, 아니면 현재 상태가 괜찮은지 명확한 기준이 없기 때문이다.
일반적으로는 GC가 50ms 이하로 완료되도록 애플리케이션 구조, 코드 작성 방식, 툴 구성 등을 점검하고 조정하는 것이 권장된다.
이 기준을 초과하면 GC 튜닝 또는 구조 개선을 검토해볼 필요가 있다.

3.5.1 G1 GC의 동작


이미지 출처 : https://mirinae312.github.io/develop/2018/06/04/jvm_gc.html

G1 GC는 CMS의 단점인 Fragmentation(단편화)을 줄이고, GC 부하를 낮추기 위해 고안된 방식이다.
CMS처럼 빠르게 작동하면서도 Stop-The-World 시간을 줄이고, 단편화도 최소화하고자 메모리 구조 자체를 새롭게 설계한다.

1) 메모리를 Region 단위로 나눈다

기존처럼 물리적인 Eden, Survivor, Old 영역으로 나누는 대신, 전체 힙 메모리를 바둑판처럼 나눈 블록 단위인 Region으로 쪼갠다.
이 Region은 논리적으로 Eden, Survivor, Old 역할을 부여받는다.
이렇게 하면 특정 블록만 선택해서 GC를 수행할 수 있으므로 성능과 효율이 높아진다.

2) GC 대상이 되는 Region만 골라서 처리한다

G1 GC는 전체 메모리를 한 번에 GC하지 않고, Garbage가 가장 많은 Region부터 우선적으로 처리한다.
이를 "Garbage First" 방식이라고 한다.
GC 대상이 되는 Region만 선별하여 처리하기 때문에, 전체를 멈추지 않고도 필요한 영역만 효율적으로 GC할 수 있다.

3) New 객체는 Eden Region에 들어간다

새로운 객체가 생성되면 Eden Region에 들어간다.
Eden Region이 가득 차면 Minor GC가 발생하고, 이때 Eden과 관련된 일부 Survivor Region만 GC 대상이 된다.
이 방식은 전체 Young 영역을 대상으로 하던 기존 방식보다 훨씬 적은 영역만 GC 대상이 되므로,
Stop-The-World 시간도 짧아진다.

4) Minor GC가 발생하면 어떻게 되나

Minor GC가 발생하면 가득 찬 Eden Region만 GC 대상이 된다.
그 Region에서 살아남은 객체는 빈 Survivor Region 또는 Old Region으로 복사된다.
그리고 Eden Region은 비워지고, 필요하면 역할도 바뀐다.
같은 Eden Region이라도 Garbage가 많지 않으면 GC 대상이 되지 않는다.
이런 방식 덕분에 GC 시간도 짧아지고, Stop-The-World 시간도 줄어든다.

5) 객체를 복사해서 단편화를 줄인다

GC 과정에서 살아남은 객체를 새로운 Region으로 복사하고, 기존 Region을 비우는 식으로 동작하기 때문에
메모리가 자동으로 정리(Compaction) 된다.
이런 방식은 Fragmentation을 자연스럽게 줄여준다.

6) Humongous Region이 따로 존재한다

하나의 객체 크기가 Region 크기의 50%를 넘으면, 일반 Region에 배치하지 않고
Humongous Region이라는 특수한 Region에 저장한다.
예를 들어, 한 객체가 전체 Region의 80%를 차지한다면 나머지 작은 객체들이 들어오지 못하는 문제가 생길 수 있는데,
이런 상황을 방지하기 위해 따로 분리해서 저장한다.
큰 객체로 인한 단편화 문제를 효과적으로 줄일 수 있는 방식이다.


https://xmlandmore.blogspot.com/2014/11/g1-gc-what-is-to-space-exhausted-in-gc.html

3.5.2 특징

  • 전체 메모리를 Region 단위로 나누고, 각 Region은 논리적으로 Eden, Survivor, Old 역할을 맡는다.
  • Garbage가 많은 Region만 선택해서 GC를 수행하므로, 전체 영역을 멈출 필요가 없다.
  • 필요 최소한의 Region만 처리하므로 Stop-The-World 시간이 짧다.
  • 큰 객체는 Humongous Region에 따로 저장하여 Fragmentation을 줄인다.

3.5.3 G1 GC에서의 minor GC

  • Eden Region이 가득 차면 GC를 수행하고, 살아남은 객체를 다른 Region(Survivor 또는 Old)에 복사한다.
  • GC 대상이 되는 Region만 처리하므로 빠르게 끝나고 Stop-The-World 시간도 짧다.

3.5.4 G1 GC에서의 Major GC

  • Minor GC로 메모리를 충분히 회수할 수 없거나, Garbage First 대상이 충분치 않으면 Major GC를 수행한다.
  • Garbage가 많은 Region만 선택해서 concurrent하게 처리한다.

3.5.5 설정

-XX:+UseG1GC

  • Java9 이상은 기본 GC이다.

GC 선택 가이드

현실적으로 Serial GC는 현대적인 서비스 환경에서 사용이 어렵다.
따라서 선택지는 다음 네 가지다:

  • Parallel GC
  • Parallel Old GC
  • CMS GC
  • G1 GC

이 중에서, 어플리케이션이 일반적인 API 서버나 스트리밍 서비스와 같은 유형이라면 G1 GC 사용을 권장한다.
G1 GC는 다양한 상황에서 안정적인 GC 성능을 제공하며, 대부분의 서비스 환경에서 무난하게 적용할 수 있다.

반면, 대용량 데이터를 처리하거나, OLAP 쿼리 중심의 DB 시스템처럼 복잡한 메모리 접근 패턴이 있는 경우에는
CMS GC가 더 나은 성능을 보이기도 하고, G1 GC가 유리한 경우도 있다.

이러한 경우에는 서비스 특성과 GC 튜닝 결과를 바탕으로 실험적으로 비교 테스트를 해보고 선택하는 것이 좋다.

profile
Data Analytics Engineer 가 되

0개의 댓글