가비지 컬렉션(Garbage Collection, GC) 개념

가비지 컬렉션(Garbage Collection, GC)은 컴퓨터 과학에서 메모리 관리의 한 형태입니다. 가비지 컬렉션의 주요 목적은 프로그램에서 더 이상 필요하지 않거나 사용되지 않는 객체나 포인터를 자동으로 정리하여 메모리를 재사용할 수 있게 하는 것입니다. 일부 프로그래밍 언어는 내장된 가비지 컬렉션 기능을 가지고 있지만, 다른 언어들은 사용되지 않는 메모리를 관리하기 위해 사용자 정의 함수를 필요로 합니다. 가비지 컬렉션은 프로그램에 의해 할당되었지만 더 이상 참조되지 않는 메모리를 회수하려는 시도입니다.

Java의 GC(Garbage Collection) 동작 원리

Java 프로세스가 실행되는 동안, Garbage Collection(GC)은 사용되지 않는 객체를 메모리에서 제거하여 Java 프로세스가 한정된 메모리 자원을 효율적으로 사용할 수 있도록 돕습니다. JVM에서 GC의 스케줄링을 담당함으로써, Java 개발자들은 메모리 관리에 대한 부담을 덜 수 있습니다. 즉, 개발 중 메모리 할당 및 해제를 직접 관리하지 않아도 됩니다.

JVM의 GC를 이해하기 위해서는 JVM 메모리 구조에 대한 기본 지식이 필요합니다.

JVM Heap 구조

JVM Heap 구조

Java 메모리의 각 영역에서 GC가 발생하면, 참조되지 않는 객체들은 메모리에서 제거됩니다.

Java에서 GC가 가능한 이유

GC의 도입은 weak generational hypothesis 가설에 기반합니다:

  • 대부분의 객체는 금방 접근 불가능 상태(unreachable)가 됩니다.
  • 오래된 객체에서 젊은 객체로의 참조는 매우 드뭅니다.

이 가설은 객체의 생명주기와 참조 패턴을 관찰함으로써, 효율적인 메모리 관리 전략을 수립할 수 있게 합니다.

GC의 작동 과정

GC는 사용되지 않는 객체를 메모리에서 제거하는 과정입니다. 이 과정에서 어떤 객체를 제거하고, 어떤 객체를 유지할지 결정해야 합니다. JVM은 메모리를 여러 영역으로 나누어, 객체의 “오래됨”을 판단합니다.

JVM 메모리 영역내에서 객체의 이동

JVM 메모리 영역(Heap)내에서 객체의 이동

객체는 처음 생성될 때 Young Generation 영역의 Eden에 위치합니다. Minor GC가 발생하면, 참조되지 않는 객체는 제거되고, 살아남은 객체는 Survivor 영역으로 이동합니다. 여러 번의 GC를 거쳐 살아남은 객체는 최종적으로 Old Generation 영역으로 이동합니다.

GC의 종류와 변화

JVM의 버전이 업데이트됨에 따라 다양한 GC 알고리즘이 개발되었습니다. 각각의 GC 알고리즘은 특정 상황에서 최적의 성능을 제공하기 위해 설계되었습니다.

GC 종류와 변화

JVM 버전이 올라감에 따라 여러가지 GC방식이 추가되고, 발전되어 왔다. 때문에 JVM 버전이 많이 올라간 만큼 다양한 GC 방식이 존재하며, 상황에 따라 필요한 GC 방식을 설정해서 사용할 수 있다.

Serial GC

Serial GC 적용을 위한 JVM 옵션 : -XX:+UseSerialGC

Serial 이란 단어의 의미인 ‘순차적인’ GC 방식이다.

Serial GC 그리고 다음에 나오는 Parallel GC 를 이해하기 위해서는 Mark-Sweep-Compaction 알고리즘을 알아두어야 할 필요가 있다.

Mark-Sweep-Compaction

Mark-Sweep-Compaction 이란 서로다른 다양한 GC에서 사용되는 알고리즘이다. 기본적인 GC 과정이라고 생각하면 좋을것이다.

GC가 사용되지 않는 객체를 메모리에서 제거하는 과정인만큼, GC 대상객체를 식별하고 제거하며 객체가 제거되어 파편화된 메모리 영역을 앞에서부터 채워나가는 작업을 수행하게 된다.

  • 사용되지 않는 객체를 식별하는 작업 (Mark)
  • 사용되지 않는 객체를 제거하는 작업 (Sweep)
  • 파편화된 메모리 영역을 앞에서부터 채워나가는 작업 (Compaction)

Compaction 작업의 경우 Windows의 디스크 조각 모음을 생각하면 좋을 것이다.

MarkSweepCompaction Mark-Sweep-Compaction 과정동안의 메모리 변화

Serial GC가 순차적으로 동작할 수 밖에 없는 이유는 GC를 처리하는 스레드가 하나이기 때문이다. 메모리나 CPU Core 리소스가 부족할 때 사용할 수 있을 것이다. Java가 처음 등장했던 90년대 후반의 PC들을 생각해보자.

Parallel GC

Parallel GC 적용을 위한 JVM 옵션 : -XX:+UseParallelGC, -XX:ParallelGCThreads (Minor GC 스레드 개수)

앞서 살펴본 Serial GC을 사용하던 시절보다 PC의 성능이 좋아졌다고 생각해보자. 메모리도 넉넉해졌고 CPU Core 도 좀 더 많아졌다. 이런 상황이라면 하나의 스레드로 동작했던 Serial GC를 멀티스레드로 실행하고 싶어질 것이다.

Parallel GC는 Minor GC를 처리하는 스레드를 여러개로 늘려 좀 더 빠른 동작이 가능하게한 방식이다.

SerialGC_vs_ParallelGC

Serial GC와 Parallel GC의 차이

그림을 보면 Serial GC는 GC 작업을 하는 스레드(GC Thread)가 하나이며, Parallel GC에서는 이 GC Thread가 여러개 존재한다.

이는 Parallel GC 에서의 GC 프로세스가 더 빠르게 동작할 수 있게 해주며 이러한 차이는 GC를 처리하는 동안 Java의 프로세스가 모두 멈춰버리는 Stop-The-World 현상이 나타나는 시간에도 영향을 주게된다.

즉, STW(Stop-The-World) 시간이 좀 더 적게 걸리는 Parallel GC에서의 Java 애플리케이션이 좀 더 매끄럽게 동작한다는 의미이다.

Parallel Old GC

Parallel Old GC 적용을 위한 JVM 옵션 : -XX:+UseParallelOldGC, -XX:ParallelGCThreads (Minor, Full GC 스레드 개수)

Parallel Old GC는 Parallel GC 를 조금더 업그레이드한 버전이다. 이름에서 알 수 있듯, Parallel GC에서 Old GC 알고리즘을 개선한 버전이다.

그럼 Old GC가 어떻게 개선되었는지 살펴보자.

우선 Parallel Old GC 는 Old GC 도 병렬로 수행될 수 있도록 하며, Old GC에 사용되는 스레드 수는 -XX:ParallelGCThreads 옵션을 통해 지정할 수 있다.

또한 Serial GC, Parallel GC에서 살펴보았던 알고리즘은 Mark-Sweep-Compaction 이었으며, Parallel Old GC 에서는 개선된 버전인 Mark-Summary-Compaction 알고리즘을 사용한다.

Summary 단계에서는 이미 GC가 수행된 영역에서 살아있는 객체를 식별하는 작업을 진행한다는 점이 Sweep과 다르다. Old GC 처리량을 늘려주기위한 작업이다.

CMS GC

CMS GC 적용을 위한 JVM 옵션 : -XX:+UseConcMarkSweepGC

앞서 살펴보았던 GC 보다 좀 더 개선된 방식이다. 개선이 된 만큼 성능은 좋아졌지만 GC의 과정은 좀 더 복잡해진 방식이다. CMS는 GC 과정에서 발생하는 STW(Stop-The-World) 시간을 최소화 하는데 초점을 맞춘 GC 방식이다.

다시 말하면 GC 대상을 최대한 자세히 파악한 후, 정리하는 시간(STW가 발생하는 시간)을 짧게 가져가겠다는 컨셉이다. 다만 GC 대상을 파악하는 과정이 복잡하한 여러단계로 수행되기 때문에 다른 GC 대비 CPU 사용량이 높다

CMS GC는 Initial Mark -> Concurrent Mark -> Remark -> Concurrent Sweep 과정을 거친다.

  • Initial Mark
    • GC 과정에서 살아남은 객체를 탐색하는 시작 객체(GC Root)에서 참조 Tree상 가까운 객체만 1차적으로 찾아가며 객체가 GC 대상(참조가 끊긴 객체)인지를 판단한다. 이 때는 STW 현상이 발생하게되지만, 탐색 깊이가 얕기 때문에 STW 발생 기간이 매우 짧다.
  • Concurrent Mark
    • STW 현상없이 진행되며, Initial Mark 단계에서 GC 대상으로 판별된 객체들이 참조하는 다른 객체들을 따라가며 GC 대상인지를 추가적으로 확인한다.
  • Remark
    • Concurrent Mark 단계의 결과를 검증한다. Concurrent Mark 단계에서 GC 대상으로 추가 확인되거나 참조가 제거되었는지 등등의 확인을 한다. 이 검증과정은 STW 를 유발하기 때문에 STW 지속시간을 최대한 줄이기 위해 멀티스레드로 검증 작업을 수행한다.
  • Concurrent Sweep
    • STW 없이 Remark 단계에서 검증 완료된 GC 객체들을 메모리에서 제거한다.

CMSGC

Initial Mark -> Concurrent Mark -> Remark -> Concurrent Sweep

CMS GC는 Compaction 작업을 필요한 경우에만 수행한다. 즉, 연속적인 메모리 할당이 어려울 정도로 메모리 단편화가 심한 경우에만 Compaction 과정을 수행하는 것이다.

G1 GC (G1: Garbage First)

G1 GC 적용을 위한 JVM 옵션 : -XX:+UseG1GC

하드웨어가 발전되면서 Java 애플리케이션에 사용할 수 있는 메모리의 크기도 점차 켜저갔다. 하지만 기존의 GC 알고리즘들로는 큰 메모리에서 좋은 성능(짧은 STW)을 내기 힘들었기 때문에 이에 초점을 둔 G1 GC가 등장하게 되었다.

즉, G1 GC는 큰 힙 메모리에서 짧은 GC 시간을 보장하는데 그 목적을 둔다.

G1 GC는 앞서 살펴본 GC와는 다른 방식으로 힙 메모리를 관리한다. 앞서 살펴보았던 Eden, Survivor, Old 영역이 존재하지만 고정된 크기로 고정된 위치에 존재하는 것이아니며, 전체 힙 메모리 영역을 Region 이라는 특정한 크기로 나눠서 각 Region의 상태에 따라 그 Region에 역할(Eden, Survivor, Old)이 동적으로 부여되는 상태이다.

JVM 힙은 2048개의 Region 으로 나뉠 수 있으며, 각 Region의 크기는 1MB ~ 32MB 사이로 지정될 수 있다. (-XX:G1HeapRegionSize 로 설정)

G1 GC가 설정된 JVM의 힙 메모리 영역의 스냅샷은 아마도 아래와 같을 것이다.

G1Heap

G1 GC가 적용된 JVM Heap 구조

G1 GC에서는 그동안 봐왔던 Heap 영역에서 보지 못한 Humongous, Available/Unused 이 존재하며 두 Region에 대한 역할은 아래와 같다.

  • Humongous : Region 크기의 50%를 초과하는 큰 객체를 저장하기 위한 공간이며, 이 Region 에서는 GC 동작이 최적으로 동작하지 않는다.

  • Available/Unused : 아직 사용되지 않은 Region을 의미한다.

G1 GC에서 Young GC 를 수행할 때는 STW(Stop-The-World) 현상이 발생하며, STW 시간을 최대한 줄이기 위해 멀티스레드로 GC를 수행한다. Young GC는 각 Region 중 GC대상 객체가 가장 많은 Region(Eden 또는 Survivor 역할) 에서 수행 되며, 이 Region 에서 살아남은 객체를 다른 Region(Survivor 역할) 으로 옮긴 후, 비워진 Region을 사용가능한 Region으로 돌리는 형태 로 동작한다.

G1 GC에서 Full GC 가 수행될 때는 Initial Mark -> Root Region Scan -> Concurrent Mark -> Remark -> Cleanup -> Copy 단계를 거치게된다.

  • Initial Mark
    • Old Region 에 존재하는 객체들이 참조하는 Survivor Region 을 찾는다. 이 과정에서는 STW 현상이 발생하게 된ㄷ.
  • Root Region Scan
    • Initial Mark 에서 찾은 Survivor Region에 대한 GC 대상 객체 스캔 작업을 진행한다.
  • Concurrent Mark
    • 전체 힙의 Region에 대해 스캔 작업을 진행하며, GC 대상 객체가 발견되지 않은 Region 은 이후 단계를 처리하는데 제외되도록 한다.
  • Remark
    • 애플리케이션을 멈추고(STW) 최종적으로 GC 대상에서 제외될 객체(살아남을 객체)를 식별해낸다.
  • Cleanup
    • 애플리케이션을 멈추고(STW) 살아있는 객체가 가장 적은 Region 에 대한 미사용 객체 제거 수행한다. 이후 STW를 끝내고, 앞선 GC 과정에서 완전히 비워진 Region 을 Freelist에 추가하여 재사용될 수 있게 한다.
  • Copy
    • GC 대상 Region이었지만 Cleanup 과정에서 완전히 비워지지 않은 Region의 살아남은 객체들을 새로운(Available/Unused) Region 에 복사하여 Compaction 작업을 수행한다.

Alt G1FullGC

JDK 7에서 G1 GC 가 정식으로 등장한 이후 많이 사용되고 있는 만큼 설정 옵션을 상세히 살펴보자

Option Default Description
-XX: G1HeapRegionSize   Region 크기. 1MB~32MB 범위에서 설정 가능. 최소 힙크기를 2048 개의 Region 으로 나눌 수 있도록 설정해야함
-XX:MaxGCPauseMillis 200 G1 GC가 유발하는 STW의 최대 시간. G1은 설정값을 최대한 맞추려고 노력할 뿐이며, 보장되는 값은 아니다.
-XX:DefaultMinNewGenPercent 5 Young 영역으로 사용할 힙 최소 크기 (전체 힙 크기대비 비율, %)
-XX:DefaultMaxNewGenPercent 60 Young 영역으로 사용할 힙 최대 크기 (전체 힙 크기대비 비율, %)
-XX:ParallelGCThreads   STW 상황에서 GC를 수행하는 스레드 개수. CPU core 수가 8개 이하인 경우 core 수와 동일하게 설정하는 것이 좋다.
-XX:ConcGCThreads   Concurrent Mark 를 수행하는 스레드 개수. ParallelGCThreads의 25% 로 설정하는 것이 좋다
-XX:InitiatingHeapOccupancyPercent 45 힙을 전체 크기 대비 특정 비율(%)만큼 사용하게될 경우 Mark 를 수행해야한다는 옵션
-XX:G1OldCSetRegionLiveThresholdPercent 65 Mixed GC 가 시작되는 Old Region 크기 비율

결론

Java의 GC는 메모리 관리를 자동화하여 개발자의 부담을 줄여줍니다. 각 GC 알고리즘은 특정 상황에서 최적의 성능을 제공하기 위해 설계되었으며, 애플리케이션의 요구 사항에 맞게 적절한 GC 알고리즘을 선택하는 것이 중요합니다.

참고자료

(1) Garbage Collection Definition - TechTerms.com. https://techterms.com/definition/garbage_collection. (2) Garbage Collection: Definition, Examples - DevX. https://www.devx.com/terms/garbage-collection/. (3) What is Garbage Collection (GC)? - Definition from Techopedia. https://www.techopedia.com/definition/1083/garbage-collection-gc-general-programming. (4) Garbage collection (computer science) - Wikipedia. https://en.wikipedia.org/wiki/Garbage_collection_%28computer_science%29. (5) 가비지 컬렉션(GC, Garbage Collection) 총정리 - 코딩팩토리. https://coding-factory.tistory.com/829. (6) Naver D2 - Java Garbage Collection - GC 알고리즘의 발전 단계 및 알고리즘별 차이점이 잘 정리되어 있음 (7)JVM 튜닝 - G1 GC 알고리즘의 동작 방식을 중점적으로 설명하고 있음 (8) 국씨의 메모장 - G1 GC - G1 GC와 연관된 용어 설명이 잘 되어 있음

태그: ,

카테고리:

업데이트:

댓글남기기