본문 바로가기
Java

[Java] Garbage Collection(가비지 컬렉션)의 개념 및 동작 원리, GC 알고리즘, G1 GC

by 위대한초밥V 2023. 9. 22.

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

C언어에서는 malloc() 함수로 할당한 메모리를 사용하지 않을 때, 개발자가 직접 free() 함수로 해제한다. Java는 개발자가 메모리를 직접 해제해 주는 경우가 없다. 그 이유는 JVM의 가비지 컬렉터가 불필요한 메모리를 정리해 주기 때문이다. 이번 글에서는 가비지 컬렉션의 개념과 동작 원리, Java9 이후 기본 GC 알고리즘인 G1 GC에 대해 알아보자.

 

먼저 가비지(Garbage)는 유효하지 않은 메모리로, 더 이상 참조되지 않아 사용하지 않은 메모리이다. 이때 가비지 컬렉션(Garbage Collection, gc라고도 한다)는 메모리 누수를 방지하기 위해 주기적으로 메모리를 청소한다.

 

그럼 메모리는 언제 점유되고, Garbage는 언제 생성되는 것일까?

Integer number = new Integer();

Integer 타입의 number라는 객체가 생성되었고, 메모리를 점유했다.

// Person 클래스 객체 생성과 Garbage
Person person = new Person();
person.setName("Judy");   // 객체 속성 설정
person = null; // person 변수가 더 이상 해당 객체를 참조하지 않는다. 

더 이상 프로그램에서 접근할 수 없으며, Garbage Collector는 나중에 수거한다.

// 새로운 객체 생성과 Garbage
person = new Person();   // 새로운 Person 객체 생성 
person.setName("July");   // 객체 속성 설정

새로운 Person 객체가 생성되었다. 기존 person 변수의 객체에 대한 참조는 끊어지고, 나중에 메모리에서 해제된다. 이렇게 Garbage는 객체가 더 이상 프로그램에서 사용되지 않을 때 발생하고, Java의 GC는 객체를 식별하고 메모리에서 정리한다.

 

+) System.gc()를 이용해 Garbage Collection을 호출할 수 있지만, 시스템 성능에 큰 영향을 미치므로 호출하면 안 된다.

System.gc()는 Major GC를 호출하기 때문에, stop the world(가비지 컬렉션을 실행하기 위해 JVM이 애플리케이션의 실행을 멈추는 작업)를 발생시키기 때문이다. 그렇지만, 서버 시작 후 메모리를 정리하거나 메모리 누수 분석 등에는 활용될 수 있다. 실제 프로덕션 코드에는 적용하지 않는다!

자세한 내용은 baeldung - Guide to System.gc()을 참고해라!


Minor GC, Major GC

Java Runtime data area

Run-Time Data Area는 JVM이 프로그램을 수행하기 위해 os에서 할당받는 메모리 영역을 말한다.

  • Method Area
  • Heap
  • Java Stack
  • PC registe
  • Native Method Stack

Java의 Heap 영역은 프로그램이 실행되면서 동적으로 생성된 객체(인스턴스)가 저장되는 공간이다. 여기서 gc가 발생하는 부분이 바로 힙 영역이다.(gc 튜닝은 힙 크기를 조정하고, 상황에 맞는 gc를 선택하는 것에 있다!)

Heap Structure

Heap 영역은 2가지를 전제로 설계되었다.

  • 대부분의 객체는 금방 접근 불가능한 객체가 된다.
  • 오래된 객체에서 새로된 객체로의 참조는 드물게 발생한다.

⇒ 객체는 대부분 일회성이며, 메모리에 오랫동안 남아있는 경우는 드물다.

따라서 다음과 같이 생존 기간에 따라 Young, Old 영역으로 설계되었다. (Permanent 영역은 Java8부터 제거되었다.)

Young 영역(Young Generation)

  • 새로운 객체가 할당되는 영역이다.
  • 대부분의 객체가 금방 Unreachable 상태가 되어, 많은 객체가 Young 영역에 생성되고 사라진다.
  • Young 영역에 대한 gc를 Minor GC라고 한다. (Minor GC는 Young 영역이 꽉 차면 발생한다)
  • Young 영역에서 살아남은 객체들은 Old 영역으로 이동한다.

Old 영역(Old Generation)

  • Young 영역에서 Reachable 상태를 유지하여 살아남은 객체들이 이동한 영역이다.
  • Young 영역보다 크게 할당되고, 크기가 큰 만큼 가비지가 적게 발생한다.
  • Old 영역에 대한 gc를 Major GC라고 한다.

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

 

Old 영역에 있는 객체가 Young 영역의 객체를 참조한다면?

Old 영역의 카드 테이블(Card Table)을 참고해라.

card table은 비트 집합으로 되어있어, 전체 메모리를 뒤질 필요 없이 특정 구간의 집합만 검색하면 된다.

 

Card Table에 대해 더 알고 싶다면?

👉 세대 별 GC(Garbage Collection) 방식에서 Card table의 사용 의미 읽어보기


Garbage Collection의 동작 방식

Young 영역과 Old 영역이 서로 다른 메모리 구조로 되어있어, 세부적인 동작은 다르다. 하지만 기본적인 공통 단계는 다음과 같다.

  • Stop The World
  • Mark and Sweep

Stop the World

가비지 컬렉션을 실행하기 위해 JVM이 애플리케이션의 실행을 멈춘다.

  • GC가 실행되면 GC를 실행하는 스레드를 제외한 모든 스레드의 작업이 중단된다.
  • GC가 완료되면 작업이 재개된다.
  • GC 성능 개선을 위한 튜닝에는 stop-the-world 시간을 줄이는 것이 중요하다.

Mark and Sweep

Stop The World로 모든 작업이 중단되면, GC는 스택의 모든 변수와 Reachable 객체를 스캔하여 어떤 객체를 참고하는지 탐색한다.

  • Mark: 사용되는 메모리와 사용되지 않는 메모리를 식별한다.
  • Sweep: Mark 단계에서 사용되지 않으므로 식별된 메모리를 해제한다.

 

Minor GC의 동작 방식(Young 영역에서 발생하는 GC)

Young 영역은 Eden 영역, Survivor 영역(2개)으로 나뉜다.

  1. 새로 생성된 객체는 대부분 Eden 영역에 위치한다.
  2. Eden 영역에서 GC가 발생하면, 살아남은 객체는 Survivor 영역 중 한 곳으로 이동한다. (stop and copy)
    • 이미 살아남은 객체가 있는 Survivor 영역으로 이동한다.
  3. 하나의 Survivor 영역이 가득 차면, 살아남은 객체들은 다른 Survivor 영역으로 이동한다. 다른 한 곳은 아무 데이터도 없는 비어있는 상태가 된다.
  4. 1,2,3이 반복되어 살아남은 객체는 Old 영역으로 이동한다.(Promotion)

⇒ 이 과정에서 Survivor 영역 중 하나는 반드시 비어 있는 상태로 남아있어야 한다.

 

Old 영역으로 이동은 어떻게 결정되는 것일까?

Object Header에 기록된 Minor GC에서 객체가 살아남은 횟수(age)로 결정한다. (옮겨진 객체의 age가 1 증가한다)

 

Minor GC가 발생하여 Old 영역까지 데이터가 쌓이는 과정이다.

HotSpot VM(CPU 코어가 하나뿐인 사용자를 위해 만들어진 클라이언트 컴파일러)에서는 빠른 메모리 할당을 위해 bump-the-pointer, TLABs(Thread-Local Allocation Buffer)라는 기술을 사용한다.

 

bump-the-pointer

  1. Eden 영역에 할당된 영역의 맨 위(top)에 있는 마지막 객체를 추적한다.
  2. 그다음에 생성되는 객체의 크기가 Eden 영역에 넣기 적당한지 확인한다.
    • 크기가 적당하다고 판정되면 Eden 영역에 넣고, 새로 생성된 객체가 맨 위에 위치한다.

새로운 객체가 생성되면 마지막에 추가된 객체만 점검하여 빠르게 메모리 할당을 할 수 있다.

 

하지만 멀티 스레드 환경이라면?

Thread Safe(여러 스레드에서 동시에 접근해도 프로그램 실행에 문제없는 상태)하기 위해서 여러 스레드에서 사용하는 객체를 Eden 영역에 저장하려면 락(lock)이 발생하여, 성능이 떨어질 것이다.

 

TLABs

락 발생으로, 성능이 떨어지는 문제를 해결하기 위한 방법이다.

각각의 스레드는 각자의 Eden 영역을 갖는다. 자신이 갖는 TLAB에만 접근이 가능하여, bump-the-pointer를 사용해도 락없이 메모리 할당이 가능하다.

 

Major GC의 동작 방식(Old 영역에서 발생하는 GC)

Young 영역에서 오래 살아남은 객체는 Old 영역으로 이동한다. Major GC는 객체들이 계속 이동하여 Old 영역의 메모리가 부족해지면 발생한다.

Young 영역은 Old 영역보다 영역의 크기가 작은만큼 0.5초에서 1초 사이에 GC가 끝난다. Major GC는 영역이 커서 더 시간이 오래 걸려, 애플리케이션 성능에 영향을 줄 수 있다. Old 영역과 Young 영역에 GC가 동시에 발생한다면 Full GC라고 한다.

이 영역은 GC 알고리즘에 따라 처리 절차가 달라진다.


GC Algorithm

GC의 종류는 다음과 같다.

  • Serial GC
  • Parallel GC
  • Parallel Old GC(Parallel Compacting GC)
  • Concurrent Mark & Sweep GC
  • G1(Garbage First) GC

Serial GC (-XX:+UseSerialGC)

Young 영역은 Mark Sweep 방식으로 수행된다. (Mark: 메모리 식별하고, Sweep: 해제)

Old 영역에서는 Mark Sweep Compact 알고리즘이 사용된다. 기존 방식에서 Heap 영역을 정리하기 위해 유효한 객체들이 연속되게 쌓이도록 힙의 가장 앞부분부터 채워서 객체가 존재하는 부분과 존재하지 않는 부분으로 나눈다.

메모리와 CPU 개수가 적을 때, 적합한 방식이다. 데스크톱의 CPU 코어가 하나만 있을 때 사용하기 위한 방식으로, 운영 서버에서는 절대로 사용하지 않는다.

 

Parallel GC(-XX:+UseParallelGC)

Serial GC와 기본 알고리즘은 같지만, GC를 처리하는 스레드가 하나가 아니라 여러 개이다. 따라서 Serial GC보다 빠르게 객체를 처리할 수 있다. 메모리가 충분하고 코어의 개수가 많을 때 유리하며, Throughput GC라고도 한다.

옵션을 통해 애플리케이션 최대 지연시간과 수행할 스레드의 개수를 설정할 수 있다.

GC의 오버헤드를 줄여주어 Java8까지 기본 가비지 컬렉터(Default Garbage Collector)로 사용되었지만, 여전히 Application이 멈추는 상황이 발생해 새로운 알고리즘이 나온다.

 

Parallel Old GC(-XX:+UseParallelOldGC)

Parallel GC와 비교했을 때 Old 영역의 GC 알고리즘이 Mark-Summary-Compaction 단계를 거친다는 점에서 다르다. Mark-Sweep-Compact 방식이 단일 스레드가 old 영역을 검사한다면, Mark-Summary-Compact 방식은 여러 스레드로 old 영역을 탐색한다.

  • Mark: Old 영역을 region 별로 나누고, region 별로 살아있는 객체를 식별한다.
  • Summary: region별 통계정보로 살아있는 객체 밀도를 나타내는 dense prefix를 정한다. 오랜 기간 참조된 객체는 앞으로 사용할 확률이 높다고 가정하고 dense prefix를 기준으로 compact 영역을 줄인다.
  • Compact: compact 영역을 destination과 source로 나누어 살아있는 객체는 destination으로 이동시키고, 참조되지 않은 객체는 제거한다.

 

CMS(Concurrent Mark & Sweep) GC(-XX:+UseConcMarkSweepGC)

CMS(Concurrent Mark Sweep) GC는 Parallel GC와 마찬가지로 여러 개의 스레드를 이용한다. Serial GC나 Parallel GC와 다르게 Mark Sweep 알고리즘을 동시에 수행한다.

  • Initial Mark: 클래스 로드에서 가장 가까운 객체 중 살아있는 객체만 찾는다. → 멈추는 시간이 매우 짧다.
  • Concurrent Mark: 방금 살아있다고 확인한 객체에서 참조하고 있는 객체들을 따라가며 확인한다. → 다른 스레드가 실행 중인 상태에서 동시에 진행된다.
  • Remark: Concurrent Mark 단계에서 새로 추가되거나 참조가 끊긴 객체를 확인한다.
  • Concurrent Sweep: 다른 스레드가 실행되고 있는 상황에서, 쓰레기를 정리하는 작업을 실행한다.

이러한 특징으로 stop-the-world 시간이 매우 짧다.

하지만 다른 GC 방식보다 메모리와 CPU를 많이 필요하고, Compaction 단계를 수행하지 않는다. 또 조각 메모리가 발생해 Compaction 작업을 실행하면 다른 GC 보다 stop-the-world 시간이 길다.

+) CMS GC는 Java9버전부터 deprecated되었고, Java14에서는 사용중지 되었다.

 

G1(Garbage First) GC

G1(Garbage First) GC는 CMS GC를 대체하기 위한 GC이다. CMS GC보다 효율적으로 Application 실행과 GC를 진행하며, 메모리 Compaction 과정이 지원되어 Java9 버전부터 기본 GC로 채택되었다.

Heap 영역을 물리적으로 메모리 공간을 나누지 않고, Region(지역) 개념을 도입하여 Heap을 균등하게 여러 개의 지역으로 나누고, 각 지역을 논리적으로 구분하여 할당한다.

G1 GC에는 Eden, Survivor, Old에 더해 Humongous, Available/Unused 역할이 존재한다.

  • Eden, Survivor, Old: 기존 GC 알고리즘의 영역
  • Humongous: Region 크기의 50%를 초과하는 객체를 저장하는 영역
  • Available/Unused: 사용되지 않은 Region

 

Minor GC

  • 한 지역에 객체를 할당하다가 해당 지역이 꽉 차면 다른 지역에 객체를 할당하고, Minor GC 실행
  • G1 GC는 가비지가 가장 많은(Garbage First) 지역에서 Mark and Sweep을 수행한다.
  • Eden 지역에서 GC가 수행된다.
    • Mark: 살아남은 객체 식별
    • Sweep: 메모리 회수
  • 살아남은 객체를 다른 지역으로 이동시킨다.
    • 복제되는 지역이 Available/Unused 지역인 경우: Survivor 영역이 된다.
    • Eden 영역인 경우: Available/Unused 지역이 된다.

1. Young Generation in G1

heap이 약 2000개의 영역으로 나뉜다. 최소 크기는 1Mb, 최대 크기는 32Mb이다.

  • 녹색: Young Generation / 파란색: Old Generation
  • Old Generation이 더 넓은 영역을 차지하는 것을 확인할 수 있다.
  • 이전 gc처럼 영역들이 연속되지 않아도 된다.

2. A Young GC in G1

Young Generation에 있는 살아남은 객체들을 Survivor Region이나 Old Generation으로 이동시킨다.(copy or move)

이때 STW(Stop the World)가 발생하고, Eden 영역과 Survivor 영역의 크기는 다음 Minor GC를 위해 다시 계산된다.

3. End of a Young GC with G1

녹색 영역은 Eden 영역에서 Survivor 영역으로 이동하거나, Survivor 영역에서 Survivor 영역으로 이동한 것이다.

  • Survivor 영역에서 Survivor 영역으로 이동한 것은 Survivor 0과 Survivor 1 사이의 이동을 뜻한다.

 

Major GC(Full GC)

기존 GC 알고리즘은 모든 Heap 영역에서 GC가 수행되어 시간이 오래 걸렸으나, G1 GC는 어느 영역에 가비지가 많은지 알고 있어 GC를 수행할 지역에서만 수행한다. 따라서 애플리케이션 지연이 최소화된다.

 

4. Initial Marking Phase

Old Region에 존재하는 객체들이 참조하는 Survivor Region이 있는지 파악해서 Survivor Region에 마킹한다.

5. Concurrent Marking Phase

Old Generation 내에 생존한 모든 객체를 마킹한다. Stop The World가 발생하지 않아 애플리케이션 스레드와 동시에 동작하고, Minor GC와 같이 진행되어 Minor GC에 의해 중단될 수 있다.

그림의 X 표시 영역은 Garbage 상태이다.

Initial Mark 단계에서 마킹된 Survivor Region에서 Old Region에 대해 참조하고 있는 객체를 마킹한다. Initial Mark 단계에서 마킹된 Survivor Region에서 Old Region에 대해 참조하고 있는 객체를 마킹한다. 멀티 스레드로 동작해 다음 GC가 발생하기 전에 동작을 완료한다.

6. Remark Phase

X 표시된 영역들은 제거되고 다시 채워진다. Stop The World가 발생한다.

7. Copying/Cleanup Phase

살아남은 객체 비율이 낮은 영역 순서대로 수거한다. live object를 다른 영역으로 move or copy하고, garbage를 수거한다. G1 GC는 Garbage를 우선 수집하여 여유 공간을 확보한다.

8. After Copying/Cleanup Phase

Major GC가 끝나고 살아남은 객체들이 새로운 지역으로 이동하고, 메모리 Compaction이 일어나 공간들이 정리되었다.

Mixed GC

Young 영역과 Old 영역 Garbage를 수집한다.

  • Old 영역의 Garbage는 한번에 수거하기에 크므로, 8회 수행된다.

Minor GC와 수행 단계는 동일하지만, Old 영역 Garbage를 추가로 수집한다. ⇒ Minor GC와 Old 영역 GC를 혼합한 과정이다.

 

G1 GC의 장단점

장점

  • 별도의 STW(Stop The World)없이 여유 메모리 공간을 압축한다. 전체 영역을 통째로 Compaction할 필요 없고, 해당 Generation의 일부분 Region만 Compaction한다.
  • Heap 크기가 클수록 잘 동작한다.
  • Garbage로 가득찬 영역을 빠르게 회수하여, 빈 공간을 확보하므로 GC 빈도가 줄어든다.

단점

  • 공간 부족 상태를 주의한다.(Minor GC, Major GC 이후에도)
    • 이때 일어나는 Full GC는 Single Thread로 동작한다.
    • Full GC는 heap 전반적으로 GC가 발생하는 것을 말한다.
  • 작은 Heap 공간에서는 제 성능이 발휘되지 못하고 Full GC가 발생한다.
  • Humonguous 영역이 최적화되지 않은 상태에서, 해당 영역이 많으면 성능에 떨어진다.
⚡️ Summary ⚡️ 
📌 Garbage Collection은 무엇인가요?(언제 발생하는지, 대상은 무엇인지)
가비지 컬렉션은 Heap 메모리에 동적 할당되었으나, 참조되지 않은 대상을 탐지하여 메모리에서 해제하는 JVM 기능을 말합니다. c언어처럼 사용하지 않은 메모리를 free 함수를 호출해 해제하지 않고 JVM이 자동으로 수행합니다. 
GC 대상은 동적으로 생성되었으나, 더 이상 참조되지 않은 객체를 말합니다.(null이 되었거나)

📌 GC 동작이 어떻게 이루어지는지 설명해보세요.(Heap 메모리 구성, Minor GC과 Major GC, 동작방식-stop the world, mark and sweep)
[Heap 메모리 구성과 Promotion]
GC가 발생하는 Heap 메모리는 Young Generation, Old Generation으로 나뉩니다.
Young Generation은 1개의 Eden 영역과 2개의 Survival1 영역으로 나뉩니다.
객체는 최초로 Eden 영역에 할당되고, GC가 거듭되면 Eden에서 Survival 영역으로, Survival 영역에서 Old Generation으로 이동합니다. (이 과정은 Young Generation 영역에서 Old Generation으로 이동하는 것이기도 합니다.) 이것을 Promotion이라고 합니다.
이처럼 Young Generation에서 이뤄지는 GC는 Minor GC, Old Generation에서 이뤄지는 GC를 Major GC, Full GC라고 합니다.
Young 영역과 Old 영역은 서로 다른 메모리 구조로 되어있어, 세부적인 동작은 다르지만 공통 단계로 Stop the World와 Mark and Sweep이 있습니다. Stop the world는 가비지 컬렉션을 실행할 때, JVM이 애플리케이션의 실행을 멈추는 것을 말합니다. GC 성능 개선을 위한 튜닝에 stop the world 시간을 줄이는 것이 중요합니다. 
Mark and Sweep은 사용되는 메모리와 사용되지 않은 메모리를 식별하고, 사용되지 않는 식별된 메모리를 해제하는 것을 말합니다.

[Old 영역으로 이동은 어떻게 결정되는 것일까요?]
Object Header에 기록된 Minor GC에서 객체가 살아남은 횟수(age)로 결정됩니다. Eden 영역에서 Minor GC가 발생했을 때, 살아남은 객체는 Age가 계속 증가합니다.

[왜 힙 메모리가 Young Generation과 Old Generation으로 구분되어있나요?]
메모리를 효율적으로 관리하기 위함으로, 객체의 생애주기에 관련있습니다. 
GC가 설계될 때, 고려한 점은 크게 두가지입니다. 1) 대부분의 객체는 금방 unreachable하게 된다. 2) Old generation의 객체가 Young Generation 객체를 참조할 일은 드물다.
1) 대부분의 객체는 금방 GC 대상이 된다는 말은, GC를 자주 수행하는 것이 메모리에 효율적임을 의미합니다. 또 소수의 객체만 오랫동안 참조가 되므로 이런 객체들은 따로 보관해 GC 대상이 되지 않도록 관리하여, 성능을 최적화할 수 있습니다.
2) Minor GC는 자주 이뤄지고, 공간을 적게 차지하여 처리 속도가 중요합니다. Major GC는 공간을 많이 차지하므로 공간적 효율성에 집중한 알고리즘이 적용됩니다.

📌 GC 알고리즘에는 무엇이 있죠? 각 알고리즘을 한줄로 간략하게 설명해 보세요.
Serial GC, Parallel GC, Parallel Old GC, CMS(Concurrent Mark & Sweep) GC, G1(Garbage First) GC가 있습니다.
- Serial GC: 단일 스레드로 수행되므로 CPU Core가 한 개인 환경에서도 사용할 수 있습니다. Mark Compact Collection 알고리즘을 사용하고, 메모리 파편화 방지를 위해 Compact 기능이 추가되었습니다. 
- Parallel GC: 다중 스레드로 수행되어, Serial GC보다 빠릅니다. Java 1.8의 기본 GC입니다.
- Parallel Old GC: Parallel GC와 비교하여 Old 영역의 GC 알고리즘이 Mark-Summary-Compaction 단계를 갖습니다.
- CMS GC(Concurrent Mark&Sweep): Stop the world 시간이 짧습니다. 다른 GC보다 메모리와 CPU를 많이 필요로 하고, Compaction 단계를 수행하지 않ㅅ습니다. 조각 메모리가 자주 발생하 Compaction 작업을 실행하면 다른 GC보다 Stop the world 시간이 깁니다.
- G1(Garbage First) GC: Heap 메모리 영역을 바둑판처럼 나누어 관리합니다. 전체 영역을 모두 Compaction하지 않고, 필요한 부분한 Compaction합니다. Java9부터 기본 GC로 채택되었습니다.

 

Reference

반응형