티스토리 뷰

728x90
반응형

가비지 컬렉션(Garbage Collection)이란?

가비지 컬렉션이란 말 그대로 ’쓰레기 수집’이다. 여기서 쓰레기란 사용하지 않는 메모리 영역이고, 이들을 수집해서 제거하는 것이 가비지 컬렉션이다.

사실 자바에서만 국한된 기능이 아니라 예전부터 컴퓨터 과학 분야에서 사용되던 방식이다. 가비지 컬렉션은 메모리 관리 기법 중 하나로, 프로그램이 동적으로 할당했던 메모리 영역 중 필요없게 된 영역을 해제하는 기능이다.

이를 자바에서는 JVM(Java Virtual Machine)이 대신 수행한다. 때문에 개발자가 메모리 누수(Memory Leak)을 크게 신경쓰지 않아도 된다.

GC(Garbage Collection)의 대상

자바에서는 런타임 시 힙(Heap) 영역에 동적으로 메모리를 할당하게 된다. 힙에는 최상위 클래스인 Object 클래스를 상속 받는 모든 객체들과 배열 등 참조 타입이 저장되게 되며, 이 참조 타입 객체들은 참조하는 변수나 필드가 없어지면 의미없는 객체가 되기 때문에 메모리 누수(Memory leak)가 발생한다. 이런 필요없는 메모리 영역들은 추후 GC의 대상이 된다.

예를 들어, 객체가 생성된 다음 이를 참조하고 있던 참조 변수가 다른 객체를 참조하게 된다면, 이전에 참조되었던 객체는 이제 어떤 변수도 참조하고 있지 않아 GC의 대상이 된다.

Member member = new Member("회원1");
member = new Member("회원2");  // 회원1 객체가 GC 대싱이 됨
member = null;  // 회원2 객체도 GC 대상이 됨

member 참조 변수가 회원1 을 참조하고 있다가 회원2 를 참조하게 되는 순간, 회원1은 더 이상 참조하는 변수가 없으므로 GC의 대상이 된다. member 변수에 null 을 넣으면 회원2 역시 GC 대상이 된다.

이 외에도, 부모 객체가 null 이 되면 이에 포함되어 있던 객체나 자식 객체들도 GC의 대상이 되며, 블럭({}) 안에서 생성된 객체가 다른 static 변수나 다른 필드에 의해 참조되지 않는다면, 함수가 종료된 후 GC의 대상이 된다.

Java GC는 객체가 가비지인지 판별하기 위해서 reachability라는 개념을 사용한다. 어떤 객체에 유효한 참조가 있으면 'reachable'로, 없으면 'unreachable'로 구별하고, unreachable 객체를 가비지로 간주해 GC를 수행한다. 유효한 참조 여부를 파악하려면 항상 유효한 최초의 참조가 있어야 하는데 이를 객체 참조의 root set이라고 한다.
root set이 될 수 있는 요소들은 아래와 같다.

  • 자바 메서드 실행 시에 사용하는 지역 변수와 파라미터들에 의한 참조
  • 네이티브 스택, 즉 JNI(Java Native Interface)에 의해 생성된 객체에 대한 참조
  • 메서드 영역의 정적 변수에 의한 참조

Reachable 객체와 Unreachable 객체

위 그림에서 root set으로부터 시작한 참조 사슬에 속한 객체들은 reachable 객체이고, 이 참조 사슬과 무관한 객체들이 unreachable 객체로 GC 대상이다.

Heap 메모리 세부 구조

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

  • 대부분의 객체는 금방 접근 불가능 상태(unreachable)가 된다.
  • 오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다.

즉, 대부분의 객체는 일회성이며, 메모리에 오랫동안 남아있는 경우가 거의 없다는 것이다. 이 전제의 장점을 최대한 살리기 위해, Heap 영역의 물리적 공간을 2개로 나누게 된다.

  • Young 영역(Yong Generation 영역) : 새롭게 생성한 객체의 대부분이 여기에 위치한다. 대부분의 객체가 금방 접근 불가능 상태가 되기 때문에 매우 많은 객체가 Young 영역에 생성되었다가 사라진다.
  • Old 영역(Old Generation 영역) : Young 영역에서 살아남은 객체가 여기로 복사된다. 대부분 Young 영역보다 크게 할당하며, 크기가 큰 만큼 Young 영역보다 GC는 적게 발생한다.

Heap 메모리의 Young 영역과 Old 영역

Old 영역이 Young 영역보다 크게 할당되는 이유는 Young 영역의 수명이 짧은 객체들은 큰 공간을 필요로 하지 않으며 큰 객체들은 Young 영역이 아니라 바로 Old 영역에 할당되기 때문이다.

각 영역에 규칙대로 객체들이 채워지다가 가득 차게 되면 GC가 발생하게 된다. 이때 영역별로 수행되는 GC를 다르게 부른다

  • Young 영역 - Minor GC
  • Old 영역 - Major GC(Full GC)

영역의 크기차이가 있기도 하고, 무엇보다 위 전제에 의해 상대적으로 Minor GC가 Major GC 보다 빈번하게 발생한다.

Young 영역은 다시 Eden과 Survival 영역으로 나뉘게 된다.

  • Eden : 새로 생성된 객체가 위치
  • Survival : Minor GC에 의해 살아남은 객체가 위치

Survival 영역은 Survival 0과 Survival 1 나뉘어져 있으며, 항상 둘 중 하나는 비워있어야 한다는 규칙이 있다. 따라서 Young 영역은 총 3가지 영역으로 나뉘게 된다.

카드 테이블(Card Table)

앞서 2번째 전제의 예외적인 상황으로 Old 영역에 있는 객체가 Young 영역의 객체를 참조하는 경우도 존재할 수 있다. 이를 위해 Old 영역에는 512Bytes의 덩어리(Chunk)로 되어있는 카드 테이블(Card Table)이 존재한다.

카드 테이블 구조

카드 테이블에는 Old 영역의 객체가 Young 영역의 객체를 참조할 때마다 그 정보가 표시된다.

💡 카드 테이블이 도입된 이유?
Minor GC(Young 영역의 GC)가 실행될 때 Old 영역에서 Young 영역의 객체를 참조하는 경우 해제하면 안되므로 Old 영역에 존재하는 모든 객체를 검사하여 참조되지 않는 Young 영역의 객체를 식별해야하는데 이 작업이 비효율적이기 때문이다.
Minor GC가 실행될 때 카드 테이블만 조회하여 GC의 대상인지 식별할 수 있게한다.

GC 동작 과정

GC의 동작 과정에는 크게 2가지 단계가 있다.

Stop The World

STW(Stop The World)는 가비지 컬렉션을 실행하기 위해 JVM이 애플리케이션의 실행을 멈추는 작업이다. GC가 실행될 때는 GC를 실행하는 쓰레드를 제외한 모든 쓰레드들의 작업이 중단되고, GC가 완료되면 작업이 재개된다. GC의 성능 개선을 위해 튜닝을 한다고 하면 보통 STW의 시간을 줄이는 작업을 의미한다.

Mark and Sweep

STW를 통해 모든 작업을 중단시키면, GC는 스택의 모든 변수 또는 Reachable 객체를 스캔하면서 각각이 어떤 객체를 참조하고 있는 지를 탐색하게 된다. 이 과정에서 Reachable한 객체들을 식별하는 Mark를 진행한다. 이후에 Mark가 되지 않은 객체들을 메모리에서 제거하는 Sweep을 진행한다.

  • Mark : 마킹한다(찾아낸다)는 의미로, Reachable한 객체를 마킹하고, GC의 대상이 되는 Unreachable 객체를 탐색하는 작업
  • Sweep : 쓸어낸다(제거한다)는 의미로, Unreachable 객체를 Heap 메모리에서 회수하는 작업
💡Mark and Sweep 알고리즘은 Sweep 이후 생긴 빈 공간들과 앞으로 할당될 객체들의 크기가 일정하지 않기 때문에 단편화(Fragmentation)가 발생한다는 단점이 있다.

이를 해결하기 위해 Compact라는 과정을 추가한 Mark and Compact 알고리즘이 있다. Compact는 Sweep 이후 생긴 빈 공간들을 남은 객체들이 연속되게 할당되도록 하는 과정이다.

Minor GC 과정

1. 새로 생성된 객체가 Eden 영역에 할당된다.

\Minor GC 과정 - 1

2. Eden 영역이 가득 차게되면, Minor GC가 실행된다. 먼저 Unreachable 객체(회색)들과 Reachable 객체(빨간색)을 표시(Mark)하고, 메모리에서 회수(Sweep)한다. 이때 살아남은 객체들은 Survival 영역 중 한 곳으로 이동시킨다.

Minor GC 과정 - 2

3. Survival 영역 중 한 곳이 가득 찰 때까지 1~2번 과정을 반복한다. 이때 살아남은 객체들에게는 Age를 부여하고 Minor GC에서 살아남을 때마다 Age를 1씩 증가시킨다.

Minor GC 과정 - 3

4. 위 과정을 반복하다가 Survival 0이 가득 차면, 이곳에서도 Minor GC 과정을 진행해서 다른 쪽의 Survival로 이동시킨다.

Minor GC 과정 - 4

5. 이때 한 쪽의 Survival 영역은 항상 비워져 있도록 한다.

Minor GC 과정 - 5

6. Survival 영역을 옮겨가며, 살아남은 객체들은 계속해서 Age를 증가시키는 과정을 반복한다.

Minor GC 과정 - 6

7. Age가 임계값에 도달하면, 해당 객체들을 Old 영역으로 이동시킨다. 이를 Promotion 이라 한다.

Minor GC 과정 - 7

Major GC

위 Minor GC를 반복하다 보면 Old 영역도 가득 차게된다. 이때 Major GC가 실행된다.

Major GC가 발생하는 상황

동작 방식은 Minor GC와 크게 다르지는 않지만 메모리 공간이 Young 영역보다 크기 때문에 상대적으로 시간이 오래 걸린다. Minor GC가 0.5초 ~ 1초 사이에 실행된다고 하면, Major GC는 보통 10배 이상의 시간이 걸리게 된다.

문제는 GC가 실행되는 동안에 앞서 설명했던 STW가 발생하므로 애플리케이션에 큰 영향을 준다. 때문에 이를 개선하는 것이 GC의 성능 개선에 있어서 핵심이 된다. 당연하게도 JDK가 버전업을 하면서 GC의 구현 방식도 발전해 왔다.

GC 구현 방법

Serial GC

특징

  • 가장 오래된 GC 방식
  • 단일 스레드로 GC 실행 (즉, 멀티코어 CPU 활용이 어려움)
  • 작은 메모리 공간에서 효율적

사용 예시

java -XX:+UseSerialGC -jar Application.java

Parallel GC (Throughput GC)

특징

  • GC 성능보다 CPU 활용률을 최적화하기 위한 처리량(Throughput)이 중요한 경우 사용
  • Young 영역의 Minor GC를 멀티 스레드로 수행한다.

사용 예시

java -XX:+UseParallelOldGC -jar Application.java

G1 GC (Garbage First GC)

G1 GC는 힙(Heap)을 Region이라는 작은 블록으로 나누어 관리한다. 아래 그림처럼 바둑판의 각 영역에 객체를 할당하고 GC를 실행한다.

G1 GC

그러다가, 가장 먼저 회수해야 할 객체가 많은 Region을 우선적으로 GC를 실행한다. 즉, 지금까지 설명한 Young의 세가지 영역에서 데이터가 Old 영역으로 이동하는 단계가 사라진 GC 방식이다. 영역을 나누기는 하지만, 언제든 더 효율적으로 생각하는 위치로 객체를 재할당 시킬 수 있다.

G1 GC에서는 이전의 GC들처럼 일일히 메모리를 탐색해 객체들을 제거하지 않는다. 대신 메모리가 많이 차있는 영역(region)을 인식해 우선적으로 GC 한다. 결과적으로 힙 메모리 전체를 탐색하는 것이 아닌 영역 별로 병렬 탐색과 GC가 실행 된다.

특징

  • JDK 9부터 기본 GC
  • 크게 할당된 객체도 효율적으로 관리
  • 애플리케이션의 Pause Time(중단 시간)을 최소화하도록 설계 - > STW 시간을 줄이는 방식

사용 예시

java -XX:+UseG1GC -jar Application.java

ZGC

이전 세대의 G1 GC에서는 메모리를 region이라는 논리적인 단위로 구분한다. 새로운 세대의 ZGC에서는 메모리를 ZPage라는 논리적인 단위로 구분한다. ZPage의 3가지 타입은 small과 medium, large이다.

ZGC heap 영역의 메모리 구조(원본 출처:  Getting started with Z Garbage Collector (ZGC) in Java 11 [Tutorial] )

ZPage를 사용할 때 주의할 점은 large 타입의 ZPage에는 단 하나의 객체만 할당할 수 있다는 것이다. 그렇기 때문에 5MB 크기의 객체를 large 타입의 ZPage에 할당하면 large 타입 ZPage의 크기가 medium 타입 ZPage의 크기보다 작을 수 있다.

ZGC는 코드상으로 10단계에 걸쳐서 실행되는데, 크게는 coloringrelocation의 두 가지 단계로 나눌 수 있다.

  • coloring : 이전 세대 GC의 marking
  • relocation : 참조가 끊긴 객체는 해제하고, coloring된 객체들은 새로운 ZPage로 재배치하는 단계

ZGC가 실행되면 STW를 발생시키고 각 스레드가 자신의 로컬 변수를 스캔한다. 스레드에서 스캔되는 로컬 변수를 GC root라 하고, GC root set을 만든다. 스레드별 로컬 변수는 많지 않기 때문에 실행되는 시간, 즉 STW 시간은 매우 짧다.

동시에 새로운 ZPage를 만든다. 이 새 ZPage는 relocatable page라 부르며, 이후 relocation 단계에서 사용된다.

이후 멀티 스레드로 GC root에서 접근 가능한(reachable) 객체에 coloring과 remapping을 실행한다. 이때는 STW가 이미 해제된 상황이다. 따라서 STW 없이 애플리케이션 메인 스레드와 함께 coloring을 진행한다.

relocation 단계에서는 garbage를 해제할 뿐만 아니라 GC 한 사이클이 끝나고 살아 있다고 판단한 객체를 relocatable page에 재배치한다. 이 relocation을 실행할 때 다시 STW를 발생시키게 된다. 물론 이 시간도 길지 않다.

(ZGC에 대한 자세한 설명은 https://d2.naver.com/helloworld/0128759 참고)

특징

  • JDK 11+, JDK 15에 정식 채택
  • 최소한의 STW를 보장(절대 10ms를 넘지 않음)
  • 초고속 성능을 요구하는 애플리케이션에 적합
  • 최대 16TB 메모리 지원
  • GC 실행 중에도 애플리케이션이 거의 멈추지 않음

사용 예시

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

Shenandoah GC

GC를 백그라운드에서 수행하여 STW 시간을 최소화하는 방식으로 객체를 이동하면서도 애플리케이션이 계속 실행된다.

특징

  • JDK 12+
  • STW(Stop-the-world) 시간을 일정하게 유지
  • 대용량 메모리에서도 빠른 GC 수행
  • 응답 속도를 일정하게 유지하는 데 초점

사용 예시

java -XX:+UseShenandoahGC -jar Application.java

GC 알고리즘 비교 요약

GC 알고리즘 특징 추천 대상
G1 GC (기본) 일반적인 GC, 지연시간 최소화 대부분의 애플리케이션
ZGC 초저지연, 16TB 메모리 지원 금융, 실시간 시스템
Shenandoah GC 대용량 데이터 처리 최적화 빅데이터, AI 서버
Parallel GC 처리량(Throughput) 최적화 배치 작업
Serial GC 단일 스레드, 저사양 최적화 임베디드 시스템, 모바일

결론적으로 어떤 GC를 사용해야 하는지 간단히 정리해 보겠다(물론 항상 최적의 GC를 찾는다는 것은 여러 변수가 있기 때문에 상황에 따라 다를 수 있다는 점을 명심해야 한다).

  • 일반적인 애플리케이션 → G1 GC (기본 GC)
  • 초저지연 시스템 (1~2ms 미만 지연시간) → ZGC
  • 대규모 데이터 처리 (빅데이터, AI) → Shenandoah GC
  • CPU 성능을 최대로 활용해야 할 때 → Parallel GC
  • 저사양 시스템 (IoT, 모바일) → Serial GC

Java 17 이상에서는 기본적으로 G1 GC를 사용하며, 특정 요구사항에 따라 ZGC, Shenandoah GC 등을 고려한다.

References

728x90
반응형
댓글