backend

JVM의 Garbage Collector 분석 (feat. ZGC와 G1간의 차이점 비교)

downfa11 2025. 2. 26. 16:34

우리는 JVM 위에서 동작하는 다양한 언어, 프레임워크, 라이브러리나 도구 등을 포함한 광범위한 영역에서 활동한다.

이러한 JVM 생태계 전반에 대한 이해를 위해서 먼저 JVM을 깊게 이해하고자 한다.

 

 

Java Virtual Machine(JVM)의 이해

단순히 Java만 실행하는 것이 아니라 Kotlin, Scala, Groovy 등의 다양한 언어를 바이트코드로 실행하는 가상머신으로, 플랫폼 독립적인 환경을 제공한다.

  • 바이트코드 실행
  • GC를 통한 자동 메모리 관리
  • JIT 컴파일로를 활용한 런타임 최적화
  • 효율적인 쓰레드 관리

 

 

1. Java Code를 컴파일(javac)

2. JVM이 이해할 수 있는 바이트코드(.class)를 JVM의 클래스 로더(Class Loader)가 메모리에 적재

3. 런타임 메모리 영역(Method area, heap, stack, PC Register, Native Method Stack)을 사용

4. 실행 엔진(Execution Engine)이 실제 CPU에서 실행하도록 변환

  • 인터프리터 : 한줄씩 바이트코드 실행
  • JIT 컴파일러 : 자주 실행되는 코드를 네이티브 변환

 

JVM 버전별 특징

Java 8 GC 추가
Java 11 AOT(Ahead-Of-Time) 컴파일 지원
Java 17 String 최적화, ZGC 정식 지원, GraalVM 최적화
Java 21 Virtual Threads 도입, Generational ZGC 추가
GraalVM, C2 최적화

 

GraalJIT : Java로 작성되어서 C++ 기반의 C2 컴파일러보다 유지보수나 확장이 용이하다.

GraalVM : 다중 언어 지원(JavaScript, Python, Ruby), AOT 지원

 

 

JDK 17과 JDK 21은 모두 G1 Garbage Collector를 기본으로 적용하는 점을 볼 수 있다.

JDK 15부터 큰 메모리를 사용할 경우에 G1 보다 성능이 좋은 ZGC가 추가되었다.

 

JVM Heap (JDK 1.7 이하)

  • Eden : 새로 생성한 대부분의 객체가 위치하는 곳
  • S0, S1 : Eden 영역에서 GC가 한번 발생한 후 살아남은 객체들이 존재하는 곳
  • Old Memory : Young 세대가 반복되는 GC 속에서 생존한 정착지
  • Perm : Class, Method 메타데이터, static 변수, 상수들이 저장되는 곳

 

JVM Heap (JDK 1.7 이후)

 

기존 Perm 영역이 Metaspace로 변경되었다.

 

이제 Heap 영역에서 벗어나 Native 영역으로 와서 JVM에 의해 크기가 강제되지 않고, 프로세스가 이용할 수 있는 메모리 자원을 최대로 활용할 수 있다.

 

 

Garbage Collector (GC)

접근 불가능한 상태에 도달한 객체에 대하여 메모리 누적시 수거하는 작업

 

stop the world : GC 쓰레드를 제외한 모두 작업을 멈춘다.

우리가 알고 있는 GC 튜닝은 이 stop the world 시간을 줄이는 것이다.

 

  • Young Generation 영역 : 대부분의 객체가 GC되는 영역 → Minor GC 발생
  • Old Generation 영역 : Young 영역보다 크게 할당되지만 GC는 적게 발생 → Full GC

Old Generation의 Full GC는 메모리가 크고 처리할 양이 많아서 stop the world 과정이 길다.

 

Garbage Collector의 종류

  • Serial GC
    • 단일 쓰레드 방식으로 작동
  • Parallel GC
    • 멀티 쓰레드 방식으로 Young Generation에 대한 GC 병렬처리
  • Parallel Old GC
    • Old Generation도 병렬 처리(Parallel에 비해서 Old Generation 처리시 리소스 추가)
  • Concurrent Mark & Sweep GC(CMS)
    • 병렬처리를 통해 Stop the world 시간은 줄어들지만 Full GC시 성능 저하 발생
    1. Inital Mark : Young Generation 객체를 정리(stop the world)
    2. Concurrent Mark : 백그라운드에서 Heap 영역 탐색으로 객체의 생명주기를 Marking
    3. Remark : stop the world를 통해 마킹을 재확인
    4. Concurrent Sweep : Old Generation 객체를 청소
  • G1(Garbage First) GC
    • 가장 많은 Garbage가 발생한 Region을 먼저 처리하도록 설계됨
    • Heap 영역을 Region으로 나누고 각 Region에서 GC를 처리
      • Stop the world 시간을 일정하게 유지

 

G1(Garbage First) GC

JDK11 부터 공식적인 GC 알고리즘으로 적용됐으며, JDK 21까지 default로 G1를 사용한다.

Eden, Survivor, Old 영역은 고정된 크기가 아니며, 전체 Heap 메모리 영역을 Region으로 나눈 것이다.

이 Region 상태에 따라 역할이 동적으로 변동된다. (default = 전체 Heap 메모리 / 2048)

 

  • Humonogouns : Region 크기의 50%를 초과하는 큰 객체를 저장하기 위한 공간
  • Available/Unused : 아직 사용되지 않은 Region

 

G1 에서도 마찬가지로 Minor GC가 존재하며, 여기서 살아남은 객체들을 Survivor Region으로 옮긴다.

이때 Eden Region을 사용 가능한(available) Region으로 돌리는 형태로 일어난다.

Full GC 대신, IHOP(Initiating Heap Occupancy Percent) 수치를 초과하면 Concurrent Cycle 과정을 실행한다.

 

 

  1. Initial Mark : Old Region에 존재하는 객체들이 참조하는 Survivor Region을 찾는다.
  2. Root Region Scan : Survivor 객체들에 대한 스캔 작업을 실시한다.
  3. Concurrent Mark : 전체 Heap에 대한 Scan 작업을 실시하고, GC 대상 객체가 발견되지 않은 Region은 제외한다.
  4. Remark : 애플리케이션을 멈추고 최종적으로 GC 대상에서 제외할 객체를 식별한다.
  5. CleanUp : 애플리케이션을 멈추고 살아있는 객체가 가장 적은 Region에 대한 미사용 객체를 제거한다.
  6. Copy : GC 대상의 Region이지만, CleanUp 과정에서 비워지지 않은 Region의 살아남은 객체들을 새로운 Region에 복사하여 Compaction 수행

Old 영역에 대해서 GC pause(mixed)를 로그로 표시하고 Young GC가 이뤄질때 수집되도록 한다.

 

G1의 성능 튜닝

GC에 걸리는 시간을 최소화하는 목적의 GC 튜닝

로그 옵션 활성화 : -Xlog:gc*:gc.log

  • XX: InitiatingHeapOccupancyPercent : IHOP 퍼센트 조절(Marking에 해당하는 최저 임계치)
  • XX: G1HeapRegionSize : Region 영역당 하나의 사이즈 (default는 (최대 heap) / 2048)
  • XX:G1ReservePercent=10 : 공간 overflow의 위험을 줄이기 위해 항상 여유 공간을 유지할 예비 메모리(백분율)
  • XX:G1HeapWastePercent=10 : 낭비할 Heap 의 공간에 대한 백분율

 

 

ZGC

큰 메모리(8MB~16TB)에서 효율적으로 GC하기 위한 알고리즘으로, JDK 11에서 실험적으로 추가되었다가 JDK 15에서 정식 출시

 

적은 메모리나 큰 메모리에서 Stop the World 시간을 최대한 적게(10ms 이하) 가져가기 위해 제작되었다.

 

G1의 Humonogous Region에서 벌어지는 GC는 기존 Region의 크기의 제약이 있다.

ZGC에서는 이 제약을 실시간 메모리 관리 및 큰 객체 최적화를 통해 개선한 방식이다.

실시간으로 Heap 영역을 관리하고 이동 가능한 객체들을 처리

 

 

각 영역을 G1에 비해서 간단하게 구성한다.

 

이전 세대의 Garbage Collector인 G1에서는 메모리를 Region 이라는 논리적 단위로 구분했다. 새로운 ZGC는 ZPage라는 단위로 구분한다.

 

ZPage에는 uint8_t 필드를 기준으로 small, medium, large로 구분한다.

 

 

jdk/src/hotspot/share/gc/z/zPageType.hpp at master · openjdk/jdk

JDK main-line development https://openjdk.org/projects/jdk - openjdk/jdk

github.com

 

각 타입별로 들어갈 수 있는 객체의 크기를 제한하는데 다음과 같다.

 

 

페이지 타입 페이지 크기 객체의 크기 객체 조정

Small 2M 265K <MinObjAlignmentInBytes>
Medium 32M 4M 이하 4K
Large X*M 4M 초과 2M

 

Large 타입의 ZPage에는 단 하나의 객체만 할당할 수 있어서 medium 타입보다 Large타입이 더 작을 수도 있다.

 

 

Flow

  1. Mark Start : ZGC의 Root에서 가리키는 객체 Mark 표시
  2. Concurrent Mark/Remap: 객체의 참조를 탐색하면서 모든 객체에 Mark 표시
  3. Mark End : 새롭게 들어온 객체들에 대해 Mark 표시
  4. Concurrent Pereare for Relocate: 재배치하려는 영역을 찾아 Relocation Set에 배치
  5. Relocate Start : 모든 Root 참조의 재배치를 진행하고 업데이트
  6. Concurrent Relocate: 이후 Load Barriers를 사용하여 모든 객체를 재배치 및 참조 수정

 

 

Colored Pointers

변수의 포인터에서 64bit를 활용해 Marking하기에 64bit OS에서만 사용가능하다.

가상 메모리 48bit를 사용하지 않고 6bit를 colored pointer로 사용해서 GC 처리에 활용한다.

 

  • Finalizable : finalizer을 통해서만 참조되는 Object의 Garbage
  • Remapped : 재배치 여부를 판단하는 Mark
  • Marked 1 / 0 : Live Object

GC 단계마다 Colored bit에 따라서 사용하는 가상 메모리 주소 공간이 달라진다.

 

왜 하나의 물리 주소에 대해 3개의 가상 주소(marked0, marked1, remapped)가 나오게 하는가?

ZGC는 GC 과정에서 새로운 ZPage로 생존한 객체를 이동한다.

이동시키면서도 애플리케이션이 중단(STW)되지 않도록 하기 위해서 가상 메모리 주소를 활용하는거다.

 

각 단계마다 같은 객체라도 다른 가상 주소를 부여해서 객체의 최신 위치를 포인터를 수정하지 않고 빠르게 찾고, 동시성을 유지할 수 있다.

 

 

OOM 발생 가능성 - ZGC에 따른 JVM Heap maxmapcount 설정

위에서 언급한대로, 가상 주소와 물리 주소를 매핑하며 이 연산을 절약하기 위해서 모든 가상 주소에 대해 mmap을 실행한다.

 

 

따라서 RSS(Resident Set Size)가 실제 메모리 사용량보다 3배 크게 관측되기도 한다.

 

ZGC는 가상 메모리보다 3배 사용하는데, JVM에서 충분히 확보하지 않고 실행하면 OOM(Out Of Memory)로 크래시가 발생하게 된다.

 

ZGC에서 가장 작은 ZPage 크기인 2MiB만큼을 가상 메모리의 단위로 잡는다.

mmap은 OS 커널의 maxmapcount 설정만큼 실행하기에, JVM Heap에서는 maxmapcount를 최소 이 2MiB의 3배만큼은 설정해야한다.

 

const size_t required_max_map_count = (max_capacity / ZGranuleSize) * 3 * 1.2;

 

이에 대해 여유분 20%를 추가해서 (max_capacity / ZGranuleSize) * 3 * 1.2로 설정하라고 권고한다.

트래픽이 많은 환경에서 ZGC를 사용할때는 각 애플리케이션의 JVM Heap 설정에 맞게 maxmapcount 값을 수정해야한다.

 

 

Load Barriers와 상태 검사

객체가 GC에 의해서 새로운 Region으로 이동했는데도 이전 주소를 참조하면 메모리 오류가 생기지 않겠는가??

객체를 읽을때마다 실행되면서 객체의 최신 Region 위치를 확인하는 역할을 하는게 Load barrier이다.

 

 

Load barriers는 Heap 영역으로부터 참조가 일어날때마다 실행되는 코드로, ZGC가 객체를 참조하는 시점을 감지하기 위해 사용한다.

 

아래의 간단한 예제를 통해서 Heap 참조를 구분해보겠다.

String refHeap(Book book){
	return book.title; // load barrier 실행
}

void notRefHeap(Book book){
	System.out.println(refHeap(book)); // load barrier 실행 안됨
}

 

G1에서 write barriers가 참조를 해제할때 상태를 검사하는 것과 반대되는 개념이다.

 

Colored bit인 marked0, marked1을 번갈아 가며 사용하면서 remmaping이 된건지, 잘못된 객체(bad color)인지도 판단할 수 있다.

 

Coloring Pointer 정보를 통해 현 객체의 상태를 식별하고, 단계에 맞는 행동을 진행한다.

참조가 일어날때 실행되어서 Heap 메모리에 있는 객체가 참조할 수 있는 상태인지 검사한다.

‘참조하기 전에 방어막을 펼친다’

 

 

ZGC는 G1과 다르게 메모리 재배치 과정에서 Stop the world 없이 재배치한다.

 

 

 

ZGC의 처리 방식

10단계의 복잡한 ZGC의 처리 방식에 대해 알아보겠따.

phase 1~5는 Coloring, phase 6~10은 Relocation으로 나눌 수 있다.

여기서 말하는 Coloring은 이전 세대 GC들이 하던 Marking과 동일하다.

 

Relocation 단계는 Coloring 단계를 거친 객체들을 재배치하는 단계다.

  • relocation : 식별된 도달할 수 없는(unreachable) 객체들을 해제하고, 살아있는 객체들은 새로운 ZPage로 옮기는 과정(compacting)
  • remmaping : 새로운 ZPage로 옮겨져서 새집(address)를 찾을 수 있도록 forwarding table을 할당하는 과정
    • load barrier 과정에서 주기적으로 remapping이 진행된다.

reclaim : 메모리를 해제하거나 옮겨서 텅 빈 ZPage를 회수하는 과정

 

 

advanced Z Garbage Collector(ZGC)

Java에서 GC 대상은 참조가 끊긴 객체뿐만 아니라, 참조가 존재해도 GC될 수 있다.

ZGC에서 non-strong reference를 처리하는 과정에 대해서 더 깊게 연구하려면 아래를 살펴보자

 

 

Deep Dive into ZGC: A Modern Garbage Collector in OpenJDK | ACM Transactions on Programming Languages and Systems

ZGC is a modern, non-generational, region-based, mostly concurrent, parallel, mark-evacuate collector recently added to OpenJDK. It aims at having GC pauses that do not grow as the heap size increases, offering low latency even with large heap sizes. The .

dl.acm.org

 

 

 

G1와 ZGC의 차이점

ZGC는 포인터를 이용해서 객체를 관리한다. (메모리 크기가 큰 경우 효율적)

 

stop the world 시간이 최악의 경우인 G1와 비교해서 1000배까지 차이난다.

 

 

 

출처 및 인용.

https://coding-start.tistory.com/205

https://d2.naver.com/helloworld/0128759

https://www.packtpub.com/en-us/learning/how-to-tutorials/getting-started-with-z-garbage-collectorzgc-in-java-11-tutorial/

https://www.baeldung.com/jvm-zgc-garbage-collector

https://blogs.oracle.com/javamagazine/post/understanding-the-jdks-new-superfast-garbage-collectors