본문 바로가기
Language/Java

[Java] Garbage Collection(GC) 가비지 컬렉션

by 계범 2022. 4. 3.

Garbage Collection이란

Java Application은 JVM위에서 구동되고, JVM이 Java Application에서 사용되는 메모리들을 관리한다.

이 JVM의 기능 중 하나로 더 이상 사용하지 않는 메모리를 청소하여 메모리 공간을 확보하는 GC라는 작업이 있다.

 

GC(Garbage Collection)은 JVM이 주기적으로 heap영역에 할당된 객체들 중 사용하지 않는 객체들을 정리해주는 작업이다.

 

GC 과정

GC를 알기 전에 'stop-the-world'를 알아야한다.

 

stop-the-world

GC를 실행하기 위해 JVM이 어플리케이션의 실행을 멈추는 것을 의미한다.

stop-the-world가 발생하면 GC를 실행하는 쓰레드를 제외한 나머지 쓰레드는 모두 작업을 멈춘다.

GC 작업이 완료한 이후에 중단되었던 작업들을 다시 시작한다.

(GC튜닝이란 대부분 이 stop-the-world 시간을 줄이는 것)

 

가비지 컬렉터(Garbage Collector)

Java에서는 개발자가 프로그램 코드로 메모리를 명시적으로 해제하지 않기때문에

가비지 컬렉터가 더 이상 필요 없는 (쓰레기)객체를 찾아 지워준다.

 

가비지 컬렉터는 'weak generational hypothesis'라는 가설에 의해 생겼다.

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

 

이 가설의 장점을 최대한 살리기 위해 HotSpot JVM[각주:1]에서는 크게 2개로 물리적 공간을 나누었다.

 

  • Young 영역
    • 새롭게 생성한 객체의 대부분이 여기에 위치.
    • 대두분의 객체가 금방 접근 불가능 상태가 되기 때문에 매우 많은 객체가 Young 영역에 생성되었다가 사라짐.
    • 이 영역에서 객체가 사라질때 Minor GC가 발생한다고 말함.
  • Old 영역
    • 접근 불가능 상태로 되지 않아 Young영역에서 살아남은 객체가 여기로 복사.
    • 대부분 Young영역보다 크게 할당.
    • 크기가 큰 만큼 Young 영역보다 GC는 적게 발생.
    • 이 영역에서 객체가 사라질 때 Major GC(혹은 Full GC)가 발생한다고 말함.

 

영역별 데이터 흐름.

Naver O2 참조

Allocation: 할당
Promotion: 일정 age값에 도달하여 Old Generation으로 이동하는 과정.

위 그림의 Permanent Generation 영역(Perm 영역)은 Method Area라고도 한다(JVM 메모리구조 참조)

객체나 억류(intern)된 문자열 정보를 저장하는 곳이다

이 영역에서도 GC가 발생하면 Major GC의 횟수에 포함된다

 

 

 

Naver O2 참조

카드 테이블(512바이트 chunk)에는 Old 영역에 있는 객체가 Young 영역의 객체를 참조할 때마다 정보가 표시된다.

Young영역의 GC를 실행할 때에는 Old영역에 있는 모든 객체의 참조를 확인하지 않고, 이 카드 테이블만 뒤져서 GC대상인지 식별한다.

 

카드 테이블은 write barrier를 사용하여 관리한다.

write barrier는 Minor GC를 빠르게 할 수 있도록 하는 장치이다.

write barrier때문에 약간의 오버헤드는 발생하지만 전반적인 GC시간은 줄어들게 된다.

 

Young 영역 구성 및 GC

young 영역은 Eden영역 1개와 Survivor 영역 2개로 구성된다.

 

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

 

Survivor 영역 중 하나는 반드시 비어 있는 상태로 남아 있어야함.

 

이렇게 Minor GC를 통해서 Old 영역까지 데이터가 쌓인 것을 간단히 나타내면 아래와 같음.

Naver O2 참조

 

HotSpot JVM에서는 보다 빠른 메모리 할당을 위해 두가지 기술을 사용한다.

 

  • bump-the-pointer
    • Eden 영역에 할당된 마지막 객체를 추적(마지막 객체는 Eden 영역의 맨 위(top)에 있음)
    • 그 다음에 생성되는 객체가 있으면, 해당 객체의 크기가 Eden 영역에 넣기 적당한지 확인.
    • 해당 객체의 크기가 적당하다고 판정되면 Eden 영역에 넣고, 새로 생성된 객체가 맨 위에 있게 됨.
    • 따라서, 새로운 객체를 생성할 때 마지막에 추가된 객체만 점검하면 되므로 매우 빠르게 메모리 할당이 이루어짐.
    • 하지만 멀티 스레드 환경에선 락이 발생하므로 , lock-contention때문에 성능이 매우 떨어지게 됨.
  • TLABs(Thread-Local Allocation Buffers)
    • 각각의 스레드가 각각의 몫에 해당하는 Eden 영역의 작은 덩어리를 가질 수 있게 하는 기술.
    • 각 쓰레드에는 자기가 갖고 있는 TLAB에만 접근할 수 있기 때문에, bump-the-pointer라는 기술을 사용하더라도 아무런 락이 없이 메모리 할당 가능

 

Old 영역에 대한 GC

Old 영역은 기본적으로 데이터가 가득 차면 GC 실행.

 

GC종류

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

 

Serial GC (-XX:+UseSerialGC)

데스크톱의 CPU 코어가 하나만 있을 때 사용하기 위해 만든 방식. 사용하면 애플리케이션의 성능이 많이 떨어짐.

Young 영역의 GC는 앞과 같고, Old 영역은 mark-sweep-compact라는 알고리즘을 사용.

 

mark-sweep-compact알고리즘

  1. Old 영역에 살아 있는 객체를 식별(Mark)
  2. 힙(heap)의 앞 부분부터 확인하여 살아 있는 것만 남김(Sweep)
  3. 각 객체들이 연속되게 쌓이도록 힙의 가장 앞 부분부터 채워서 객체가 존재하는 부분과 객체가 없는 부분으로 나눔(Compaction)

Serial GC는 적은 메모리와 CPU 코어 개수가 적을 때 적합한 방식

 

Parallel GC (-XX:+UseParallelGC)

Java 8의 default GC

기본 알고리즘은 Serial GC와 동일.

다른 점은 Parallel GC는 Young영역에서 GC를 처리하는 쓰레드가 여러개이다.(Serial GC에 비해 처리속도 향상)

 

메모리가 충분하고 코어의 개수가 많을 때 유리. Throughput GC라고도 부름

 

Parallel Old GC(-XX:+UseParallelOldGC, -XX:+ParallelGCThreads=n)

Old 영역에도 멀티 스레드 방식을 사용. -XX:+ParallelGCThreads=n 옵션으로 멀티 스레드 개수 지정.

Old GC에서 Mark-Summary-Compaction 단계를 거치는 방식.

Summary 단계는 앞서 GC를 수행한 영역에 대해서 별도로 살아 있는 객체를 식별한다는 점에서 Sweep과 다르며, 약간 더 복잡한 단계를 거치게 됨.

 

CMS GC (-XX:+UseConcMarkSweepGC)

Serial GC와 CMS GC 비교. 출처 ORACLE

Stop-the-world 시간을 줄인 GC. Low Latency GC라고도 부름.

 

4단계를 거침.

  • Inital Mark : 클래스 로더에서 가장 가까운 객체 중 살아 있는 객체만 찾는 것으로 끝냄.(stop-the-world 발생)
  • Concurrent Mark : 윗 단계에서 확인한 객체에서 참조하고 있는 객체들을 따라가 확인.(stop-the-world 발생 X)
  • Remark : Concurrent Mark단계에서 새로 추가되거나 참조가 끊긴 객체 확인(stop-the-world 발생)
  • Concurrent Sweep : 쓰레기를 정리하는 작업을 실행(stop-the-world 발생 X)

단점

  • 다른 GC 방식보다 메모리와 CPU를 더 많이 사용.
  • Compaction 단계가 기본적으로 제공되지 않음
  • 조각난 메모리가 많아 Compaction 작업을 실행하면 다른 GC방식의 stop-the-world 시간보다 더 길어질 수 있음.

 

JDK 1.7 버전 이전 VS JDK 1.8 이후 Heap 메모리 구조

 

JDK 1.7 이전 heap
jdk 1.7 이전과 jdk1.8 이후

Perm 영역이 Metaspace 영역으로 변경됨.

 

구분 Perm MetaSpace
저장 정보 클래스 meta / 메서드 meta / static 변수,상수 클래스 meta / 메서드 meta
관리 포인트 Heap 영역 튜닝 + Perm 영역 별도 Native 영역 동적 조정
GC Major GC Major GC
메모리 측면 -XX: PermSize / -XX: MaxPermSize -XX: MetaSpaceSize / -XX: MaxMetaspaceSize

 

가장 중요한 건 Heap에서 관리되었던 Perm영역이 MetaSpace로 변경되면서 Native 영역으로 바뀜.

Native영역의 특징 중 하나는 JVM에 의해 크기가 강제되지 않고, 프로세스가 이용할 수 있는 메모리 자원을 최대로 활용할 수 있음.

 

 

G1 GC

출처: "The Garbage-First Garbage Collector" (TS-5419), JavaOne 2008, p. 19)

 

JDK11 default GC. CMS GC를 개선하기위해 만든 GC.

앞서 말했던 Young 영역과 Old 영역대신 heap을 Region이란 특정한 크기로 나누고,

Region의 상태에 따라 역할(Eden,Survivor, Old)이 동적으로 변동한다.

(Region은 기본적으로 (전체 Heap 메모리) / 2048 이 default 값으로 지정)

 

Heap을 Region이라는 일정한 부분으로 나눠서 메모리를 관리하고,

GC대상 객체가 발견된 각각의 Region에만 GC 발생한다.

 

지금까지의 GC 중 가장 성능이 좋음.

 

G1GC Heap
G1GC 과정

  • Initial Mark : Old Region에 존재하는 객체들이 참조하는 Survivor Region을 찾는다(STW)
  • Root Region Scan : 위에서 찾은 Survivor 객체들에 대한 스캔 작업을 실시한다
  • Concurrent Mark : 전체 Heap의 scan 작업을 실시하고, GC 대상 객체가 발견되지 않은 Region은 이후 단계를 제외한다
  • Remark : 애플리케이션을 멈추고(STW) 최종적으로 GC 대상에서 제외할 객체를 식별한다
  • Cleanup : 애플리케이션을 멈추고(STW) 살아있는 객체가 가장 적은 Region에 대한 미사용 객체를 제거한다
  • Copy : GC 대상의 Region이었지만, Cleanup 과정에서 완전히 비워지지 않은 Region의 살아남은 객체들을 새로운 Region(Available/Unused) Region에복사하여 Compaction을 수행한다(STW)

살아있는 객체가 아주 적은 Old 영역에 대해 [GC pause(mixed)] 를 로그로 표시하고, Young GC가 이루어질 때 수집되도록 한다

 

ZGC

ZGC는 JDK 15버전에서 준비중인 GC.

큰 메모리(8MB ~ 16TB)에서 효율적인 GC하기 위한 알고리즘.

 

ZGC Heap

ZGC에선 2개의 주요 알고리즘이 있다.

 

Colored Pointers

Colored Pointers

객체를 가리키는 변수의 포인터에서 64bit을 활용해서 Marking을 함.

  • Finalizable : finalizer을 통해서만 참조되는 객체로, 해당 Pointer가 Mark되어 있으면 Garbage 대상임.
  • Remapped: 재배치 여부를 판단하는 Pointer. 해당 Pointer가 Mark되어 있으면 최신 참조 상태임을 의미.
  • Marked 1/0 : Live 객체인지 확인하는 Pointer

이 비트들로  ZGC가 객체를 찾고,마킹하고, 재배치 하는 등의 작업을 지원한다.

32Bit 플랫폼에서는 사용 불가. 64비트의 운영체제에서만 가능

 

Load Barriers

Load Barriers

Load Barrier는 JIT가 특정 위치에 주입한 코드를 말하는데, 이 코드를 통해 참조가 연결되는 객체의 Meta bits 상태를 확인한다.

ZGC는 G1GC와 다르게 메모리를 재배치하는 과정에 위에서 언급한 Bit를 통해 STW없이 재배치를 하는데,

이때 RemapMark와 Relocation Set을 확인하여 참조 값과 Mark 상태를 업데이트할 수 있다.

 

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

 

G1GC와의 차이점은 Pointer를 이용해서 객체를 Marking하고 관리하는 것.

ZGC의 목표는 각각의 STW 시간을 (< 10ms) 이하로 줄이는 것!

 

각 GC의 성능 비교

 

오라클의 설명을 보면 ZGC는 확장 가능하고 저지연 GC로 설명되어있음.

일시 중지 시간은 사용 중인 힙 크기와 무관하다고 나온다.

 

또한 3가지의 대표 기능이 있는데,

1. 힙 크기 설정

2. 동시 GC 스레드 수 설정

3. 사용하지 않은 메모리를 운영 체제로 반환할 수 있다고 한다.

 

더 깊은 공부는 아래 참조 링크들 참조 바람...

 

GC와 Java Reference, Reachability 참조 링크

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

 

참조

네이버 D2

GC 종류 참조 블로그

G1GC와 ZGC 참조 블로그

ZGC 참조 블로그2

ZGC 동작원리 참조 블로그

오라클 ZGC 참조

  1. HotSpot JVM은 미국의 LLC라는 회사에서 처음 발표한 JVM으로 현재는 가장 일반적인 JVM 중 하나.
    Hot한 Spot을 찾아서 해당 부분에서는 JIT 컴파일러를 사용하는 방법.(다른 곳은 인터프리터)
    참조 블로그 [본문으로]

댓글