가비지 컬렉터 종류

maketheworldwise·2022년 2월 13일
0


이 글의 목적?

가비지 컬렉터에는 다양한 종류가 있다. 그리고 어떤 종류를 사용하느냐에 따라 내부적으로 돌아가는 로직이 다르다 하니 한번 알아보고자 한다. 자세히는 아니더라도 개념 틀을 잡는 정도로만 정리해보자.

stop-the-world

GC가 실행되면 stop-the-world가 발생한다. stop-the-world가 발생하면 GC를 실행하는 스레드를 제외한 나머지 스레드는 모두 작업을 멈추고, GC 작업을 모두 완료한 뒤 중단되었더 작업들이 재개된다. stop-the-world는 어떤 GC 알고리즘을 사용하더라도 발생하며, GC 튜닝은 이러한 stop-the-world 시간을 줄이는 것에 목적이 있다.

자바에서는 메모리를 명시적으로 해지하지 않는데, 명시적으로 해지하기 위해서는 다음과 같은 방법이 필요하다.

  • 해당 객체 참조에 null 지정
  • System.gc() 호출 (사용해서는 안됨)

GC 알고리즘

GC 알고리즘의 기본 흐름은 GC의 대상을 식별하고, 식별된 대상을 메모리에 제거하며, 필요한 경우 최적화까지 수행한다.

Reference Counting Algorithm

GC 대상 탐색에 초점을 맞춘 초기 알고리즘이다. 각 객체마다 Reference Count를 관리하는데, 이는 참조되고 있는 개수를 의미하고, 이 개수가 0일 때 GC를 수행한다. 단순한 구조이고 개수가 0이 되면 즉시 메모리에서 해제되어 실시간 애플리케이션 실행에 영향을 주지 않는다는 장점이 있으나, 최신 상태로 계속 관리해주어야 하고, 유지 비용이 크게 발생한다는 단점이 있다. 대표적으로 LinkedList 같은 순환 구조에서 메모리 누수가 발생할 가능성이 있다.

Mark-Sweep-Compaction Algorithm

기본적인 GC 과정으로, Reference Counting 알고리즘의 단점을 해결하기 위해 나온 - 다양한 GC에서 사용되는 초창기 알고리즘이다. GC 대상을 식별(Mark)하고 청소(Sweep)하고 압축(Compaction)한다. Root Set에서 시작하는 Reference 관계를 추적하며, 트레이싱 알고리즘이라고도 한다.

식별 단계에서는 GC 대상 외 살아남을 객체를 마킹하고, Header에 Flag를 심거나 별도의 BitmapTable을 이용한다.

청소 단계에서는 식별 단계가 끝나면 바로 실행되며, 마킹이 없는 객체를 모두 삭제하는 작업을 수행한다. 청소가 완료되면 살아남은 모든 마킹 정보를 초기화한다.

압축 단계에서는 청소 단계 후 살아남은 객체들 사이사이의 빈 공간(단편화를 살아남은 객체들을) 이어 붙여 해결한다. 이 작업 이후 살아남은 객체들의 Reference를 업데이트하는 작업(객체의 참조값이 실제 메모리의 위치값이기에 참조 값을 변경하고 수정하는 작업)이 필요하며 부가적인 중단 시간과 오버헤드가 발생된다.

Copy-Scanvenge Algorithm

실제 메모리 영역을 논리적으로 객체가 할당되는 Active 영역과 InActive 영역으로 분리하여 Active 영역 내에서 접근 가능한 객체들을 마킹하고 이것들을 InActive 영역으로 복사한 뒤 기존 영역의 접근 불가능한 객체들을 해제하는 방식이다. 복사된 이후에는 객체 간의 참조 값을 업데이트하면서 한쪽 끝으로 순서대로 할당하게 된다. 이 방식의 GC는 일반적으로 Active 영역에 객체가 할당되지 못하는 경우 발생하며, 논리적으로 실제 메모리 영역을 분할하기 때문에 메모리 영역의 크기만큼 객체를 할당할 수 없다. JVM의 Minor GC도 이 알고리즘을 기반으로 동작한다.

Concurrent-Mark-Sweep Algorithm

기존의 Mark-Sweep 알고리즘 방식을 사용하는 대신에 최대한 작업을 애플리케이션과 동시 수행시키며 발생하는 전체적인 stop-the-world 시간을 감소시키는데 중점을 둔 방식이다. JVM의 CMS GC가 이 알고리즘을 기반으로 동작한다.

GC 종류

JVM에는 많은 종류의 GC 알고리즘이 존재한다. Java 7과 Java 8은 Parallel GC를 사용하고, Java 9와 Java 10은 G1 GC를 사용한다. Java 11부터는 실험적 기능인 Z GC를 사용할 수 있다. Java 15부터는 정식 기능으로 출시할 예정이라고 한다.

Serial GC

# Serial GC 사용 옵션
-XX:+UseSerialGC

순차적인 GC로 Mark-Sweep-Compaction 알고리즘이 한 번에 하나씩만 동작한다. 적은 메모리와 CPU 코어 개수가 적을 때 적절한 방식이다. 가장 오래된 GC이며, 최근에는 사용하지 않고 stop-the-world 시간이 길어 사용해서는 안되는 GC가 되었다.

Parallel GC

# Parallel GC 사용 옵션
-XX:+UseParallelGC

Serial GC가 하나의 스레드로 Mark-Sweep-Compaction을 수행한다면, Parallel GC는 여러 개의 스레드로 Mark-Sweep-Compaction을 수행한다. Serial GC보다 속도면에서 빠르다. Parallel GC는 메모리가 충분하고 코어의 개수가 많을 때 유리하며, Throughtput GC라고도 한다.

Parallel Old GC

# Parallel Old GC 사용 옵션
-XX:+UseParallelOldGC

Parallel GC와는 비슷하나, Mark-Sweep-Compaction 알고리즘이 아닌 Mark-Summary-Compaction 알고리즘을 사용한다. Summary 단계는 Sweep 단계에서 살아있는 객체를 식별하는 작업이 추가된 것이다.

CMS GC

# CMS GC 사용 옵션
-XX:+UseConcMarkSeppGC

Parallel GC 보다 더 개선되고 stop-the-world 시간을 최소화하는데 초첨을 맞춘 복잡해진 GC다. GC가 대상 객체를 최대한 자세히 파악한 후, stop-the-world가 발생하는 Sweep 시간을 최소화하는데 큰 목적을 두었다. 따라서 모든 애플리케이션의 응답 속도가 매우 중요할 때 사용하는 GC로 Low Latency GC라고도 부른다.

GC 과정 중 진행중이던 스레드가 정지하지 않기 때문에 stop-the-world 시간이 짧다.

CMS GC는 총 4단계에 걸쳐 이루어진다.

  1. Initial Mark
    GC 과정에서 살아남을 객체를 Root Set에서 가장 가까운 객체만 탐색하며, 참조가 끊겼는지를 확인하는 단계다. stop-the-world가 일어나지만, 탐색 깊이가 짧아 멈추는 시간도 짧다.

  2. Concurrent Mark
    Initial Mark 단계에서 GC 대상으로 판별한 객체들을 따라가며 GC 대상인지 추가로 확인한다. 이 과정에서는 stop-the-world가 발생하지 않는다.

  3. Remark
    Concurrent Mark 단계의 결과를 검증한다. 이 단계에서는 GC 대상으로 추가 확인되었는지, 참조가 제거되었는지 등 확인을 한다. stop-the-world가 일어나며, 이 시간을 최대한 줄이기 위해 멀티스레드로 검증작업을 수행한다.

  4. Concurrent Sweep
    GC 대상으로 판별된 객체들을 멀티스레드로 메모리에서 제거한다. stop-the-world가 발생하지 않는 단계다.

매우 빠른 응답 속도를 가짐에도 불구하고 다음과 같은 단점이 존재한다.

  • 다른 GC 방식보다 메모리와 CPU를 더 많이 사용한다.
  • Compaction 단계가 기본적으로 제공되지 않는다.
  • 조각난 메모리가 많아져 Compaction 작업을 실행하면 다른 GC 방식의 stop-the-world 시간보다 더 오래 걸린다.

따라서 CMS GC는 단순히 빠르다고 사용하면 안되고, Compaction 작업이 얼마나 자주, 오랫동안 수행되는지 확인해야 한다.

G1 (Garbage First) GC

# G1 GC 사용 옵션
-XX:+UseG1GC

G1 GC는 CMS GC를 대체하기 위해 등장했고, 대용량의 메모리가 있는 멀티 프로세스 시스템을 위해 만들어졌다. 빠른 처리 속도와 stop-the-world를 최소화한다. 성능적으로 가장 뛰어나며, Java 7부터 정식으로 포함되었고 Java 9부터는 기본 GC가 되었다.

기존의 GC와는 다르게 Young 영역과 Old 영역을 물리적으로 나누지 않고 힙의 일정한 크기의 Region이라는 논리적 단위로 나누어서 관리한다. G1 GC는 가비지가 많을 것으로 예상되는 회수 가능한 영역에 대한 수집과 압축이 주 목적이다. 따라서 G1 GC는 가비지만 있는 Region을 처음에 수거하기 때문에 Garbage First라는 이름이 붙었다. CMS GC와 다르게 Compaction 단계를 진행해 메모리 단편화를 없앴다.

다른 GC는 고정된 메모리 크기의 Young, Old, Metaspce 영역으로 나뉘어졌지만, G1 GC는 동일한 크기의 힙 영역 집합으로 분할한다. 이미지에서 확인할 수 있듯이, Eden, Survivor, Old 영역 외에도 Humongous, Available/Unused Region이 추가적으로 존재하는 것을 볼 수 있다. Humongous는 Region 크기의 50%를 초과하는 큰 객체를 저장하기 위한 공간으로, 이 Region에서는 GC 동작이 최적으로 동작하지 않는다. Available/Unused Region은 아직 사용되지 않는 공간을 의미한다.

출처: https://velog.io/@hanblueblue/GC-1.-G1GC#g1gc

Young에서 GC가 수행되면 stop-the-world가 발생하며, 이 시간을 줄이기 위해 멀티 스레드로 GC를 수행한다. 동작 방식은 기존과 비슷하다. Eden과 Survivor 영역에서 살아남은 객체들은 다른 Survivor 영역으로 이동하며, 비워진 Region은 Available/Unused 상태로 변경된다.

출처: https://velog.io/@hanblueblue/GC-1.-G1GC#g1gc

Full GC가 수행될 때는 총 6개의 단계를 거치게 된다.

  1. Initial Mark
    Old Region에 존재하는 객체들이 참조하는 Survivor Region을 찾는다. 이 단계에서는 stop-the-world가 발생한다.

  2. Root Region Scan
    Initial Mark에서 찾은 Survivor Region에서 GC 대상 객체를 탐색한다.

  3. Concurrent Mark
    전체 Region에 대해 스캔하고 GC 대상 객체가 존재하지 않는 Region은 이후 단계에서 제외된다.

  4. Remark
    stop-the-world 후 GC가 대상에서 제외할 객체를 식별한다.

  5. Cleanup
    stop-the-world 후 살아있는 객체가 가장 적은 Region에 대해서 참조되지 않는 객체를 제거한다. stop-the-world 끝내고 완전히 비워진 Region을 FreeList에 추가하여 재사용한다.

  6. Copy
    Root Region Scan 단계에서 찾은 GC 대상 Region이었지만, Cleanup 단계에서 살아남은 객체들을 Available/Unused Region에 복사되여 Compaction 작업을 수행한다.

이 글의 레퍼런스

profile
세상을 현명하게 이끌어갈 나의 성장 일기 📓

0개의 댓글