Garbage Collection

최강일·2024년 11월 30일

목차

  • Garbage Collection이란
  • GC 대상
  • GC 청소 방식
  • Heap 메모리 구조
  • GC 동작 과정
  • GC 알고리즘 종류

Garbage Collection이란

가비지 컬렉션(Garbage Collectoin, 이하 GC)은 자바의 메모리 관리 방법 중의 하나로 JVM의 Heap 영역에서 동적으로 할당했던 메모리 중 필요 없게 된 객체(garbage)를 모아 주기적으로 제거하는 프로세스를 말한다.

(GC는 꼭 Java에만 있는 개념이 아니다. 파이썬, 자바스크립트, Go 언어 등 많은 프로그래밍 언어에서 가비지 컬렉션이 기본으로 내장되어 있다.
파이썬의 GC, 레퍼런스 카운팅 : 객체를 만들 때 그것이 얼마나 자주 사용되는지 카운트하는 것이다. 캐시 메모리의 교체 방식에서 LRU(Least Recently Used)가 참조 횟수를 카운트해서 victim을 고르는 것과 비슷)

Garbage Collector 장점

C / C++ 언어에서는 이러한 GC 없어 프로그래머가 수동으로 메모리 할당과 해제를 일일이 해줘야 했었다.
반면 Java에서는 가비지 컬렉터가 메모리 관리를 대신해주기 때문에 Java 프로세스가 한정된 메모리를 효율적으로 사용할수 있게 하고, 개발자 입장에서 메모리 관리, 메모리 누수(Memory Leak) 문제에서 대해 관리하지 않아도 되어 오롯이 개발에만 집중할 수 있다는 장점이 있다.

Garbage Collector 단점

그러나 단점 또한 존재한다.
자동으로 처리해준다 해도 메모리가 언제 해제되는지 정확하게 알 수 없어 제어하기 힘들며, GC가 동작하는 동안에는 다른 동작을 멈추기 때문에 오버헤드가 발생되는 문제점이 있다.
이를 Stop-The-World라 한다.

STW(Stop The World)

GC를 수행하기 위해 JVM이 프로그램 실행을 멈추는 현상을 의미한다.
GC가 작동하는 동안 GC 관련 Thread를 제외한 모든 Thread는 멈추게 되어 서비스 이용에 차질이 생길 수 있다.
따라서 이 시간을 최소화 시키는 것이 쟁점이다.
이로 인해 GC가 너무 자주 실행되면 소프트웨어 성능 하락의 문제가 되기도 하다.

GC 대상

Garbage Collection은 특정 객체가 garbage인지 아닌지 판단하기 위해서 도달성, 도달능력(Reachability)이라는 개념을 적용한다.
객체에 레퍼런스가 있다면 Reachable(객체가 참조되고 있는 상태)로 구분되고, 객체에 유효한 레퍼런스가 없다면 Unreachable(객체가 참조되고 있지 않은 상태, GC의 대상이 됨)로 구분해버리고 수거해버린다.

예제

예를들어 jVM 메모리에서는 객체들은 실질적으로 Heap영역에서 생성되고 Method Area나 Stack Area에서는 Heap 영역에 생성된 객체의 주소만 참조하는 형식으로 구성된다.
하지만 이렇게 생성된 Heap 영역의 객체들이 메서드가 끝나는 등의 특정 이벤트들로 인하여 Heap 영역 객체의 메모리 주소를 가지고 있는 참조 변수가 삭제되는 현상이 발생하면, Heap 영역에서 어디서든 참조하고 있지 않은 객체들이 발생하게 된다.
이러한 객체들을 주기적으로 GC가 제거해주는 것이다.

GC 청소 방식

Mark-Sweep라는 알고리즘은 다양한 GC에서 사용되며 객체를 솎아내는 내부 알고리즘이다.

Mark-Sweep


GC가 될 대상 객체를 식별(Mark)하고 제거(Sweep)하며 객체가 제거되어 파편화된 메모리 영역을 앞에서부터 채워나가는 작업(Compaction)을 수행하게 된다.

  • Mark 과정 : 먼저 Root Space로부터 그래프 순회를 통해 연결된 객체들을 찾아내어 각각 어떤 객체를 참조하고 있는지 찾아서 마킹한다.
  • Sweep 과정 : 참조하고 있지 않은 객체 즉 Unreachable 객체들을 Heap에서 제거한다.
  • Compact 과정 : Sweep후에 분산된 객체들을 Heap의 시작 주소로 모아 메모리가 할당된 부분과 그렇지 않은 부분으로 압축한다.(GC의 종류에 따라 수행하지 않는 경우도 있음)

GC의 Root Space

Heap 메모리 영역을 참조하는 method 영역, static 변수, stack, native method stack이다.


Garbage Collection 이라고 하면 garbage 들을 수집할 것 같지만 실제로는 garbage 를 수집하여 제거하는 것이 아니라, garbage 가 아닌 것을 따로 mark 하고 그 외의 것은 모두 지우는 것이다.

Heap 메모리 구조

JVM의 Heap 영역은 동적으로 레퍼런스 데이터가 저장되는 공간으로, GC 대상이 되는 영역이다.
Heap 영역은 처음 설계될 때 다음의 2가지를 전제로 설계되었다.

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

즉, 객체는 대부분 일회성이며, 메모리에 오랫동안 남아있는 경우는 드물다는 것이다.
이러한 특성으로 객체의 생존 기간에 따라 물리적인 Heap영역을 나누게 되었고, Young과 Old 총 2가지 영역으로 설계되었다.

Old 영역 메모리가 큰 이유

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

Heap 영역의 물리적 분리(Young, Old)

Young 영역

  • 새롭게 생성된 객체가 할당되는 영역
  • 대부분의 객체가 금방 Unreachable 상태가 되기 때문에, 많은 객체가 Young 영역에 생성되었다가 사라진다.
  • Young 영역에 대한 가비지 컬렉션을 Minor GC라고 부른다.

Old 영역

  • Young 영역에서 Reachable 상태를 유지하여 살아남은 객체가 복사되는 영역
  • Young 영역보다 크게 할당되며, 영역의 크기가 큰 만큼 가비지는 적게 발생한다.
  • Old 영역에 대한 가비지 컬렉션(Garbage Collection)을 Major GC 또는 Full GC라고 부른다.

Young Generation 분리

또다시 더욱 효율적인 GC를 위해 Young 영역을 3가지 영역(Eden,survivor 0,survivor 1)으로 나눈다.
이렇게 하나의 Heap 영역을 세부적으로 쪼갬으로서 객체의 생존 기간을 면밀하게 제어하여 GC를 보다 정확하게 불필요한 객체를 제거하는 프로세스를 실행하도록 한다.

Eden

  • new를 통해 새로 생성된 객체가 위치
  • 정기적인 가비지 수집 후 살아남은 객체들은 Survivor 영역으로 보냄

Survivor 0 / Survivor 1

  • 최소 1번의 GC 이상 살아남은 객체가 존재하는 영역
  • Survivor 영역에는 특별한 규칙이 있는데, Survivor 0 또는 Survivor 1 둘 중 하나에는 꼭 비어 있어야 한다.

GC 동작 과정

JVM > Runtime Data Area > Heap > Old/Young > Eden/Survivor 0,1까지 구조를 알아보았다.
이제 GC가 동작하는 과정을 알아본다.

Minor GC 동작 과정

Young Generation 영역은 짧게 살아남는 메모리들이 존재하는 공간이다.
모든 객체는 처음에는 Young Generation에 생성된다.
Young Generation의 공간은 Old Generation에 비해 상대적으로 작기 때문에 메모리 상의 객체를 찾아 제거하는데 적은 시간이 걸린다. 이 때문에 Young Generation 영역에서 발생되는 GC를 Minor GC라 불린다.

  1. 처음 생성된 객체는 Young Generation 영역의 일부인 Eden 영역에 위치
  2. 객체가 계속 생성되어 Eden 영역이 꽉차게 되고 Minor GC가 실행
  3. Mark 동작을 통해 reachable 객체를 탐색
  4. Eden 영역에서 살아남은 객체는 1개의 Survivor 영역으로 이동
  5. Eden 영역에서 사용되지 않는 객체(unreachable)의 메모리를 해제(sweep)
  6. 살아남은 모든 객체들은 age값이 1씩 증가
  7. 또다시 Eden 영역에 신규 객체들로 가득 차게 되면 다시한번 minor GC 발생하고 mark 한다
  8. marking 한 객체들을 비어있는 Survival 1으로 이동하고 sweep
  9. 다시 살아남은 모든 객체들은 age가 1씩 증가
  10. 이러한 과정을 반복

age 값이란
Survivor 영역에서 객체의 객체가 살아남은 횟수를 의미하는 값이며, Object Header에 기록된다.
만일 age 값이 임계값에 다다르면 Promotion(Old 영역으로 이동) 여부를 결정한다.
JVM 중 가장 일반적인 HotSpot JVM의 경우 이 age의 기본 임계값은 31이다.
객체 헤더에 age를 기록하는 부분이 6 bit로 되어 있기 때문이다.

Major GC 동작 과정

Old Generation은 길게 살아남는 메모리들이 존재하는 공간이다.
Old Generation의 객체들은 거슬러 올라가면 처음에는 Young Generation에 의해 시작되었으나, GC 과정 중에 제거되지 않은 경우 age 임계값이 차게되어 이동된 녀석들이다.
그리고 Major GC는 객체들이 계속 Promotion되어 Old 영역의 메모리가 부족해지면 발생하게 된다.

Major GC는 Old 영역은 데이터가 가득 차면 GC를 실행하는 단순한 방식이다.
Old 영역에 할당된 메모리가 허용치를 넘게 되면, Old 영역에 있는 모든 객체들을 검사하여 참조되지 않는 객체들을 한꺼번에 삭제하는 Major GC가 실행되게 된다.
하지만 Old Generation은 Young Generation에 비해 상대적으로 큰 공간을 가지고 있어, 이 공간에서 메모리 상의 객체 제거에 많은 시간이 걸리게 된다.
예를들어 Young 영역은 일반적으로 Old 영역보다 크키가 작기 때문에 GC가 보통 0.5초에서 1초 사이에 끝난다.
그렇기 때문에 Minor GC는 애플리케이션에 크게 영향을 주지 않는다.
하지만 Old 영역의 Major GC는 일반적으로 Minor GC보다 시간이 오래걸리며, 10배 이상의 시간을 사용한다.
바로 여기서 초반에 소개했던 Stop-The-World 문제가 발생하게 된다.
Major GC가 일어나면 Thread가 멈추고 Mark and Sweep 작업을 해야 해서 CPU에 부하를 주기 때문에 멈추거나 버벅이는 현상이 일어난다.

  1. 객체의 age가 임계값에 도달한다.
  2. 이 객체들은 Old Generation 으로 이동된다. (이를 promotion 이라 부른다.)
  3. 위의 과정이 반복되어 Old Generation 영역의 메모리가 부족하게 되면 Major GC가 발생되게 된다.

Summary

GC 종류Minor GCMajor GC
대상Young GenerationOld Generation
실행 시점Eden 영역이 꽉 찬 경우Old 영역이 꽉 찬 경우
실행 속도빠르다느리다

GC 알고리즘 종류

점점 데이터의 양도 늘어나고 애플리케이션의 크기도 커짐에 따라 Heap 사이즈가 커지고 애플리케이션의 지연 현상이 두드러지게 되었다.
이를 최적화하기 위해 다양한 Garbage Collection 알고리즘이 개발되었다.
GC 알고리즘은 모두 설정을 통해 Java에 적용이 가능하여, 상황에 따라 필요한 GC 방식을 설정해서 사용할 수 있다.

GC 알고리즘 종류desc
Serial GC
  • CPU 코어가 1개일 때 사용하기 위해 개발된 가장 단순한 GC
  • CPU의 코어가 여러 개인 경우, 사용X
Parallel GC
  • Java 8의 기본 GC
  • Serial GC와 동작방식은 비슷하나, young 영역의 Minor GC를 멀티 쓰레드로 수행
  • GC의 오버헤드를 상당히 줄여주었으나, App이 멈추는 것을 피할 수 없었고, 개선하기 위해 다른 더 많은 알고리즘 등장
Parallel Old GC
  • Java 5부터 제공된 GC이며 Parallel GC를 개선한 버전
  • Young 영역 뿐만 아니라, Old 영역에서도 멀티 쓰레드로 GC 수행
CMS GC
  • Java 9부터 deprecated되고 Java14 부터는 사용중지
  • 앱의 쓰레드와 GC 쓰레드가 동시에 실행되어 stop-the-world 시간을 최대한 줄이기 위해 고안된 GC
  • Mark Sweep의 Compaction 단계를 수행하지 않는다는 단점. 장기적으로 조각난 메모리들이 많아진다.
  • Mark Sweep 알고리즘을 Concurrent하게 수행
G1 GC(Garbage First)
  • 장기적으로 많은 문제를 일으킬 수 있는 CMS GC를 대체하기 위해 jdk7 버전에서 최초로 release된 GC
  • Java 9+ 버전의 기본 GC로 지정
  • 4GB 이상의 heap 메모리, Stop the world 시간이 0.5초 정도 필요한 상황에 사용
  • 기존의 GC알고리즘에서는 heap 영역을 물리적으로 분리된 Young/Old 영역으로 나누어 사용하였지만, G1 gc는 다른 개념의 Region 개념을 사용
  • 전체 heap 영역을 Region이라는 영역으로 체스같이 분할하여 상황에 따라 Eden,Survivor,Old등 역할을 고정이 아닌 동적으로 부여
  • Garbage로 가득찬 영역을 빠르게 회수하여 빈 공간을 확보하므로, 결국 GC 빈도가 줄어드는 효과를 얻게 되는 원리
Shenandoah GC
  • Java 12에 release
  • 기존 CMS가 가진 단편화, G1이 가진 pause의 이슈를 해결
ZGC (Z Garbage Collector)
  • Java 15에 release
  • 대량의 메모리(8MB ~ 16TB)를 low-latency로 잘 처리하기 위해 디자인 된 GC

G1 GC 동작 방식

G1(Garbage First) GC는 장기적으로 많은 문제를 일으킬 수 있는 CMS GC를 대체하기 위해 개발되었고, Java7부터 지원되기 시작하였다.

기존의 GC 알고리즘에서는 Heap 영역을 물리적으로 Young 영역과 Old 영역으로 나누어 사용하였다.
G1 GC는 Eden 영역에 할당하고, Survivor로 카피하는 등의 과정을 사용하지만 물리적으로 메모리 공간을 나누지 않는다.
대신 Region(지역)이라는 개념을 새로 도입하여 Heap을 균등하게 여러 개의 지역으로 나누고, 각 지역을 역할과 함께 논리적으로 구분하여(Eden 지역인지, Survivor 지역인지, Old 지역인지) 객체를 할당한다.

G1 GC에서는 Eden, Survivor, Old 역할에 더해 Humongous와 Availabe/Unused라는 2가지 역할을 추가하였다. Humonguous는 Region 크기의 50%를 초과하는 객체를 저장하는 Region을 의미하며, Availabe/Unused는 사용되지 않은 Region을 의미한다.

G1 GC의 핵심은 Heap을 동일한 크기의 Region으로 나누고, 가비지가 많은 Region에 대해 우선적으로 GC를 수행하는 것이다. 그리고 G1 GC도 다른 가비지 컬렉션과 마찬가지로 2가지 GC(Minor GC, Major GC)로 나누어 수행된다.

Minor GC

한 지역에 객체를 할당하다가 해당 지역이 꽉 차면 다른 지역에 객체를 할당하고, Minor GC가 실행된다. G1 GC는 각 지역을 추적하고 있기 때문에, 가비지가 가장 많은(Garbage First) 지역을 찾아서 Mark and Sweep를 수행한다.

Eden 지역에서 GC가 수행되면 살아남은 객체를 식별(Mark)하고, 메모리를 회수(Sweep)한다. 그리고 살아남은 객체를 다른 지역으로 이동시키게 된다. 복제되는 지역이 Available/Unused 지역이면 해당 지역은 이제 Survivor 영역이 되고, Eden 영역은 Available/Unused 지역이 된다.

Major GC(Full GC)

시스템이 계속 운영되다가 객체가 너무 많아 빠르게 메모리를 회수 할 수 없을 때 Major GC(Full GC)가 실행된다. 그리고 여기서 G1 GC와 다른 GC의 차이점이 두각을 보인다.

기존의 다른 GC 알고리즘은 모든 Heap의 영역에서 GC가 수행되었으며, 그에 따라 처리 시간이 상당히 오래 걸렸다.
하지만 G1 GC는 어느 영역에 가비지가 많은지를 알고 있기 때문에 GC를 수행할 지역을 조합하여 해당 지역에 대해서만 GC를 수행한다. 그리고 이러한 작업은 Concurrent하게 수행되기 때문에 애플리케이션의 지연도 최소화할 수 있는 것이다.

Summary

물론 G1 GC는 다른 GC 방식에 비해 잦게 호출될 것이다.
하지만 작은 규모의 메모리 정리 작업이고 Concurrent하게 수행되기 때문이 지연이 크지 않으며, 가비지가 많은 지역에 대해 정리를 하므로 훨씬 효율적이다.
> 처리량과 지연 시간의 균형을 잡고 Full GC간 발생하는 Stop-The-World를 최소화

이러한 구조의 G1 GC는 당연히 앞의 어떠한 GC 방식보다 처리 속도가 빠르며 큰 메모리 공간에서 멀티 프로레스 기반으로 운영되는 애플리케이션을 위해 고안되었다. 또한 G1 GC는 다른 GC 방식의 처리속도를 능가하기 때문에 Java9부터 기본 가비지 컬렉터(Default Garbage Collector)로 사용되게 되었다.

G1 GC의 효율성

Java9+ 부터 기본 GC로 자리잡은 G1 GC에서는 이전의 GC들처럼 일일히 메모리를 탐색해 객체들을 제거하지 않는다.
대신 메모리가 많이 차있는 영역(region)을 인식하는 기능을 통해 메모리가 많이 차있는 영역을 우선적으로 GC 한다.
즉, G1 GC는 Heap Memory 전체를 탐색하는 것이 아닌 영역(region)을 나눠 탐색하고 영역(region)별로 GC가 일어난다.

또한 이전의 GC 들은 Young Generation에 있는 객체들이 GC가 돌때마다 살아남으면 Eden → Survivor0 → Survivor1으로 순차적으로 이동했지만, G1 GC에서는 순차적으로 이동하지는 않는다.
대신 G1 GC는 더욱 효율적이라고 생각하는 위치로 객체를 Reallocate(재할당) 시킨다.
예를 들어 Survivor1 영역에 있는 객체가 Eden 영역으로 할당하는 것이 더 효율적이라고 판단될 경우 Eden 영역으로 이동시킨다.

G1이 default로 채택된 이유

성능이 Shenanadoah GC, ZGC가 G1 GC보다 더 뛰어날 수 있지만 종합적으로 보았을 때 제일 안전하고 성능이 뛰어난 GC가 G1이기에 현재까지도 default GC로 자리 매김하고 있다는 결론에 도달할 수 있게 되었습니다.

G1 GC 실행 명령어

java -XX:+UseG1GC -jar Application.java

JVM 튜닝

목적

Old 영역으로 넘어가는 객체의 수를 최소화하는 것
Full GC의 실행 시간을 줄이는 것이다.

튜닝과정

  1. 튜닝전에 필요한 설정들이 되어있는지 확인한다.
  2. 그리고 GC 상황을 충분히 모니터링한다.
  3. 모니터링 결과 분석 후 GC 튜닝 여부 결정한다. GC 수행시간이 0.1~0.3s라면 굳이 튜닝할 필요 없다. 하지만 1~3초, 또는 그 이상일 경우 튜닝을 진행한다.
  4. GC 방식/메모리 크기 변경 및 지정등을 진행한다.

GC 성능을 결정하는 옵션들

  • 힙 영역 크기 : -Xms
  • 최대 힙 영역 크기 : -Xmx
  • New와 Old 영역 비율 : -XX:NewRatio
  • New 영역의 크기 : -XX:NewSize
  • Eden 영역과 Survivor 영역의 비율 : -XX:SurvivorRatio

GC의 성능에 많은 영향을 주는 GC 방식

  • G1 : -XX:+UnlockExperimentalVMOptions
  • G1 : -XX:+UseG1GC

참고

[1] https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EA%B0%80%EB%B9%84%EC%A7%80-%EC%BB%AC%EB%A0%89%EC%85%98GC-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%F0%9F%92%AF-%EC%B4%9D%EC%A0%95%EB%A6%AC

profile
Search & Backend Engineer

0개의 댓글