[Java] Garbage Collector 동작 원리와 과정 | Heap Memory, Minor/Major GC, GC algorithm

dyomi·2024년 7월 4일

가비지 컬렉터(Garbage Collector)란,

JVM내에서 메모리 관리를 해주는 것을 말하며, 일반적으로 사용되는 GC라는 말은 가비지 컬렉션을 의미한다.
가비지 컬렉션은 힙영역에 동적으로 할당했던 메모리 중 필요없게 된 메모리 객체를 모아 주기적으로 제거하는 프로세스를 말한다.

Mark and Sweep

가비지 컬렉션은 객체에 레퍼런스가 있다면 Reachable(참조되고 있는 상태)로 구분되고,
객체에 유효한 레퍼런스가 없다면 Unreachable(참조되고 있지 않은 상태)로 구분하여 수거해버린다.

이때 기본적으로 사용하는 청소 방식은 마크 앤 스윕(Mark and Sweep) 방식으로, 가비지 컬렉션의 대상 객체를 식별한 다음 제거하고, 파편화된 메모리 영역을 앞에서부터 채워나가는 방식을 말한다.

가비지 컬렉션 동작 과정

가비지 컬렉터는 아래의 두 가지 가설 하에 만들어졌다.

  1. 대부분의 객체는 금방 접근 불가능 상태(unreachable)가 된다.

  2. 오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다.

이 가설의 장점을 최대한 살리기 위해 HotSpotVM에서는 힙영역을 Young 영역Old 영역 크게 2개의 물리적 공간으로 나누었다.

그리고 위 그림과 같이 Young 영역은 하나의 Eden 영역두개의 Survivor 영역으로 나뉘게 된다.

Young Generation

Young 영역은 새롭게 생성된 객체가 할당되는 영역으로, 대부분의 객체가 금방 접근 불가능의 상태가 되기 때문에 많은 객체가 Young 영역에서 생성되었다가 사라진다.

이때 Young 영역에서 일어나는 가비지 컬렉션을 MinorGC 라고 부르며, 동작 방식은 아래와 같다.

  1. 새로 생성한 대부분의 객체는 Eden 영역에 위치한다.
  2. Eden 영역에서 GC가 한 번 발생한 후 살아남은 객체는 Survivor 영역 중 하나로 이동된다.
  3. Eden 영역에서 GC가 발생하면 이미 살아남은 객체가 존재하는 Survivor 영역으로 객체가 계속 쌓인다.
  4. 하나의 Survivor 영역이 가득 차게 되면 그 중에서 살아남은 객체를 다른 Survivor 영역으로 이동한다. 그리고 가득 찬 Survivor 영역은 아무 데이터도 없는 상태로 된다.
  5. 이 과정을 반복하다가 계속해서 살아남아 있는 객체는 Old 영역으로 이동하게 된다.

이때 Survivor 영역 중 하나는 반드시 비어 있어야 하며, 만약 두 Survivor 영역에 모두 데이터가 존재하거나, 두 영역 모두 사용량이 0이라면 현재 시스템은 정상적인 상황이 아니라고 보면 된다.

Old Generation

Old 영역은 참조 상태를 유지하며 오래 살아남은 객체가 복사되는 영역이다.

이곳에서 발생되는 가비지 컬렉션은 MajorGC 라고 불리며, Old 영역이 가득 차면 GC를 실행하는 단순한 방식이다.

Old 영역은 Young 영역보다 크게 할당되며, 크기가 큰만큼 가비지 컬렉션의 횟수는 적게 발생하지만, MinorGC가 보통 0.5~1초 사이로 끝난다면, MajorGC는 10배 이상의 시간이 소요된다.

이때 가비지 컬렉션이 동작하는 동안 다른 동작들은 멈추기 때문에 오버헤드가 발생될 수 있으며, 이를 전문 용어로 Stop-The-World라 한다.

STW (Stop-The-World)

가비지 컬렉션을 수행하기 위해 JVM이 프로그램 실행을 멈추는 현상을 의미한다.

GC가 작동하는 동안 GC 관련 Thread를 제외한 모든 Thread는 멈추게 되어 서비스 이용에 차질이 생길 수 있다.

따라서 이 시간을 최소화 시키는 것이 쟁점이며, 이처럼 애플리케이션이 지연되는 현상을 최소화 하기 위해 다양한 가비지 컬렉션 알고리즘이 존재한다.

GC algorithm

GC 알고리즘은 모두 설정을 통해 Java에 적용할 수 있으며, 프로젝트의 특성과 상황에 따라 필요한 GC 방식을 설정해서 사용할 수 있다.

1. Serial GC

서버의 CPU 코어가 1개 일때 사용하기 위해 개발된 가장 단순한 GC이다.
싱글 쓰레드로 처리하기 때문에 가장 STW 시간이 길고, 보통 실무에서 사용하는 경우는 없다.
MinorGC에는 Mark-Sweep을 사용하고, MajorGC에는 Mark-Sweep-Compact를 사용한다.

java -XX:+UseSerialGC -jar Application.java

2. Parallel GC

Java8의 디폴트 GC이다.

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

java -XX:+UseParallelGC -jar Application.java

3. Parallel Old GC (Parallel Compacting Collector)

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

java -XX:+UseParallelOldGC -jar Application.java

4. CMS GC (Concurrent Mark Sweep)

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

# deprecated in java9 and finally dropped in java14
java -XX:+UseConcMarkSweepGC -jar Application.java

5. G1 GC (Garbage First)

CMS GC를 대체하기 위해 JDK7 버전에서 최초로 release된 GC이다.
Java9+ 버전의 디폴트 GC로 지정되었고, 힙영역이 너무 작을 경우 미사용을 권장한다.
기존의 GC 알고리즘은 힙영역을 물리적으로 고정된 Young/Old 영역으로 나누어 사용하였지만, G1 GC는 Region이라는 새로운 개념을 도입하여 사용한다.
전체 힙영역을 Region이라는 체스같이 분할한 영역으로 상황에 따라 Eden, Survivor, Old 등 역할을 동적으로 부여한다.

Garbage로 가득찬 영역을 빠르게 회수하여 빈 공간을 확보하므로, GC 빈도가 줄어드는 효과를 얻게 되는 원리이다.

또한 이전의 GC 들은 Young 영역에 있는 객체들이 GC가 돌때마다 살아남으면 Eden → Survivor1 → Survivor2으로 순차적으로 이동했지만, G1 GC에서는 순차적으로 이동하지는 않는다.
대신 G1 GC는 더욱 효율적이라고 생각하는 위치로 객체를 Reallocate(재할당) 시킨다.
예를 들어 Survivor1 영역에 있는 객체가 Eden 영역으로 할당하는 것이 더 효율적이라고 판단될 경우 Eden 영역으로 이동시킨다.

java -XX:+UseG1GC -jar Application.java

6. Shenandoah gc

세넨도어GC는 Java12에 release 되었다. 레드 햇에서 개발한 GC로 기존 CMS가 가진 단편화, G1이 가진 pause의 이슈를 해결하였다.

강력한 Concurrency와 가벼운 GC 로직으로 heap 사이즈에 영향을 받지 않고 일정한 pause 시간 소요가 특징이다.

java -XX:+UseShenandoahGC -jar Application.java

7. ZGC (Z Garbage Collector)

ZGC는 Java15에 release되었고, 대량의 메모리(8MB ~ 16TB)를 low-latency로 잘 처리하기 위해 디자인 된 GC이다.

G1의 Region 처럼,  ZGC는 ZPage라는 영역을 사용하며, G1의 Region은 크기가 고정인데 비해, ZPage는 2MB 배수로 동적으로 운영된다.

ZGC가 내세우는 최대 장점 중 하나는 힙 크기가 증가하더도 'stop-the-world'의 시간이 절대 10ms를 넘지 않는다는 것이다.

java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -jar Application.java


🌟 추가 질문

📍 초기 메모리 설정과 GC 발생 시점?

만약 Java에서 가비지 컬렉터가 동작하지 않는다면, 시스템 메모리가 부족해져 OutOfMemoryError가 발생할 수 있다.

Java 애플리케이션을 실행할 때 개발자가 메모리 설정을 할 수 있으며, 따라서 애플리케이션을 만들고 배포할 때 필요한 메모리와 서버의 CPU 요구 사항을 결정할 수 있어야 한다.

이를 위해, 개발자는 본인의 애플리케이션에서 생성할 객체의 수와 각 객체의 필드 크기를 계산하여 해당 애플리케이션에 필요한 메모리를 어느정도 예측할 수 있어야 한다.

또한 GC가 일어나는 시점은 JVM이 결정하기 때문에 정확히 설정할 수는 없다.

하지만 초기 메모리 설정과 객체 생성 방식은 가비지 컬렉션의 타이밍에 큰 영향을 미치므로, 이를 조정함으로써 가비지 컬렉션이 발생하는 시점을 예측할 수는 있다.



참고자료

가비지 컬렉션 동작 원리 & GC 종류 💯 총정리
Java Garbage Collection

profile
기록하는 습관

0개의 댓글