Development/Java

JVM GC

DevKTak 2023. 2. 3. 14:06

GC(Garbage Collection)

JVM의 Heap 영역에서 사용하지 않는 객체를 삭제하는 프로세스를 말한다.

 

Java GC를 잘 알아야 하는 이유: 서비스 중에 문제가 생기면 대부분 GC의 메모리 파트에서의 발생 빈도가 높기 때문이다.

하지만 과거엔 서버를 사서 코어, 메모리 사이즈 등에 맞추어 이런 저런 GC 튜닝을 해야했지만 퍼블릭 클라우드로 바뀌면서 환경이 거의 통일 되었기에 요즘엔 GC를 튜닝할 일이 잘 없다고 한다.

 

왜 자바에서만 특히 GC를 신경쓸까?

러스트 같은 언어도 GC가 있긴한데 아무때나 아무렇게 일어나도 괜찮은 모델이다.

A a = new A();
B b = new B();
a = b;

대충 위와 같은 참조를 못하게 막아서 레퍼런스 카운트를 1로 강제하기 때문에 STW 같은것도 발생하지 않는다.

언어들마다 언어들만의 해결책이 있는데 자바는 아직 찾지 못한것 같다.

이유는 러스트처럼 해결책 변화를 주면 기존에 돌아가던 것들이 동작하지 않을 수 있기 때문에 방법은 분명있지만 선택을 못하고 있을 가능성이 있다.

 

JVM의 Heap 영역 구조

Young Generation Old Generation
Eden From Survivor
또는
S0
또는
Survivor 0
To Survivor
또는
S1
또는
Survivor 1

 

 YG와 OG로 나눈 이유는?

애플리케이션에서 객체의 수명은 대부분이 짧기 때문에 효율적으로 관리하기 위함이다.

 

 Survivor Space가 있는 이유?

Eden 영역의 메모리 단편화 문제를 해결하기 위함이다.

 

 Survivor Space가 2개인 이유?

Minor GC의 대상이 Eden에만 국한되는게 아니라 Survivor 영역까지 Minor GC를 하기 때문, Survivor 영역에도 메모리 단편화가 존재하게 된다.

 

 Old 영역에 있는 객체가 Young 영역의 객체를 참조하는 경우가 있을 때에는 어떻게 처리될까?

이러한 경우를 처리하기 위해서 Old 영역에는 512 바이트의 덩어리(chunk)로 되어 있는 카드 테이블(card table)이 존재한다.
카드 테이블에는 Old 영역에 있는 객체가 Young 영역의 객체를 참조할 때마다 정보가 표시된다. Young 영역의 GC를 실행할 때에는 Old 영역에 있는 모든 객체의 참조를 확인하지 않고, 이 카드 테이블만 뒤져서 GC 대상인지 식별한다.

 

STW(Stop the World)

GC를 실행하는 쓰레드 외의 모든 쓰레드가 작업을 중단하는 것.

  • 대개의 경우 가비지 컬렉터 튜닝이란 Stop The World 시간을 줄이는 것이다.

 

 Stop the World가 왜 필요할까?

애플리케이션 스레드가 멈추어야 현재 메모리 상에서 살아있는 객체를 정확히 식별할 수 있기 때문이다.

 

 Stop the World가 없다면?

애플리케이션의 동작에 따라 변화하는 객체들의 상태를 빠르게 반영하지 못했을 수 있다.

 

GC의 동작 순서

 GC Roots

  • Stack 영역의 데이터들
  • method 영역의 static 데이터들
  • JNI에 의해 생성된 객체들

 

 기본적으로 GC Roots로 부터 Mark and Sweep, Compact 알고리즘을 사용하여 탐색.
Reachable 한 객체는 Mark bit를 true(혹은 1)로 세팅하고 mark가 되지 않은 Unreachable한 객체들은 false(혹은 0) 으로 셋팅한 후
Sweep을 통해 Heap에서 제거하고
Compact 과정을 통해 메모리 단편화를 막아준다. 

 

GC가 일어나는 시점

 Minor GC

  1. 새롭게 생성된 객체는 전부 Eden Space에 할당된다.
  2. Eden Space에 할당할 공간이 없으면 Minor GC를 수행한다. (Stop the World 시작)
  3. 생존한 모든 객체(Mark 당한 객체, Reachable한 객체)는 Survivor Space로 복사한다.
  4. GC로부터 살아남은 객체는 Generational count가 1 증가한다. (count를 1씩 늘리는 프로세스를 aging이라고 부른다)
  5. Eden Space를 비운다.(Sweep) (Stop the World 종료)
  6. 1번 ~ 5번 과정을 반복하다가 또 Eden Space에 공간이 없어서 Minor GC가 수행되고 (Stop the World 시작) Survivor Space에 옮기려고 했더니 Survivor Space에 연속된 메모리 공간을 화보하지 못해서 더 이상 메모리 할당이 불가능하다면 From Survivor Space에서 생존한 모든 객체들을 To Survivor Space의 연속된 공간에 먼저 옮기고(generational count가 높은 객체의 수명이 더 길 가능성이 높기 때문이며 수명이 더 길 가능성이 높은 메모리를 먼저 배치하는 이유는 메모리의 단편화를 줄이기 위함이다.), 그 후에 Eden Space에서 생존한 객체를 To Survivor Space의 연속된 공간에 옮긴다.
  7. From Survivor Space와 Eden Space를 비운다.
  8. 기존 From Survivor Space의 역할을 To Survivor Space가 대신하게 됐으므로 둘의 이름을 바꾼다. (Stop the World 종료)
  9. 1번 ~ 8번 과정을 반복하다가 age가 threshold(임계점)를 넘기면 Old Generation으로 Promotion(승진)을 한다.

◈ Premature Promotion

적당한 나이(TenuringThreshold)를 먹지 않았는데 어쩔 수 없이 Old Genration으로 이동하는 행위 Premature Promotion(조기승진)이라고 부르며 다음의 경우에 발생한다.

  • Survivor Space에 적당한 공간이 없을때
  • 새롭게 할당될 객체의 용량이 Eden Space의 용량보다 클때

 

Major GC 혹은 Full GC가 일어나기 전에는 회수해 가지 않으며 적당한 나이를 먹지 않고 와서 단명할 가능성이 높음에도 불구하고 쓸데없이 Old Generation을 차지하고 있기 때문에 Major GC 혹은 Full GC의 발생 빈도를 늘려 애플리케이션 전반에 영향을 미치기 때문에 적절하게 Young Generation과 관련된 사이즈를 정하는게 중요하다.

 

 Major GC

Major GC도 Old Generation이 꽉 찼을 때 수행된다.

Old Generation은 메모리 할당률이 낮기 때문에 GC가 일어나는 빈도가 적고 용량은 Young Generation에 비해 크게 잡기 때문에 객체의 갯수가 많아서 GC 시간은 길다. (Stop the World 시간이 길다.)

 

 Full GC

  • Heap 메모리 전체 영역, Old와 Young 영역 모두에서 발생하는 GC이다.
  • Minor GC, Major GC를 수행하고 나서도 여유 공간이 부족한 경우 Full GC가 발생하고 Single Thread로 동작한다.
  • System.GC(); 명시적으로 호출시에도 발생한다.

 

GC 종류

Serial GC

  • 하나의 쓰레드로 GC를 처리한다.(싱글 쓰레드)
  • 다른 GC에 비해 Stop The World 시간이 길다.
  • Minor GC 에는 Mark-Sweep을 사용하고, Major GC에는 Mark-Sweep-Compact 사용한다.
  • Mark Sweep Compact

 

※ 왜 Major에서만 Compaction을 할까?

  • 가설에 의하면 보통 Old 영역까지 가기전에 객체가 죽기 때문이다.
  • Young 영역 같은 경우에는 Survivor Space를 둠으로써 메모리 단편화를 막았다.

 

 Parellel GC

  • Java 8의 default GC
  • Serial GC와 같은 방식이지만 YG 영역에서 멀티 쓰레드로 처리
  • Serial GC에 비해 Stop The World 시간 감소
  • Mark Sweep Compact

 

 Parellel Old GC

  • Parellel GC와 같은 방식이지만 OG 영역까지 멀티 쓰레드로 처리
  • Mark Summary Compact

* sweep: 단일 쓰레드가 old 영역 전체를 훓는다.

* summary: 멀티 쓰레드가 old 영역을 분리해서 훓는다.

 

 

 CMS(Councurrent Mark Sweep) GC

  • Tri-color Marking
  • Stop The World가 발생하는 Sweep 시간을 최소화하기 위해 고안한 방법으로 4 단계가 있다.
  1. Initial Mark: GC Root에서 참조하는 객체에만 Masking한다. (STW 발생)
  2. ConCurrent Mark: 1 단계에서 마킹한 객체가 참조하는 다른 객체들을 추적하여 마킹한다. (STW 발생안함)
  3. Remark: 2 단계에서 변경된 과정을 한번 더 마킹하며 확정시킨다. (STW 발생)
  4. Current Sweep: Mark 되지않은 Unreachable 객체들을 제거한다. (STW 발생안함)
  • 1, 2, 3, 4 단계와 같이 GC 대상 객체를 파악하는 과정이 복잡하게 여러단계로 수행되기 때문에 CPU 부하가 커지고 Compact 과정이 없어(단편화 발생)
  • Old 영역의 크기가 충분하지 않거나 크기에 비해 조각난 메모리가 많을 경우 오히려 Stop The World 시간이 늘어날 수 있다.

 

 G1(Garbage First) GC

  • CMS GC의 메모리 단편화 문제를 개선하였다.
  • Java 7에서 처음 등장한 뒤, Java 9 이후의 default GC
  • Heap을 Region이라는 단위로 나눈 뒤, Eden, Survivor, OG Region으로 나눈다.
  • Region의 목표 수치는 2048으로 분활된다. 즉, 8G의 Heap이라면 하나의 Region의 크기는 4MB(8192MB/2048 = 4MB)이다.
  • Garbage만 남아있는 Region을 먼저 회수한다고 해서 Garbage First라는 이름이 붙었다.
    빈 공간 확보를 더 빨리 한다는 것은 조기 승격이나 급격히 할당률이 늘어나는 것을 방지하여 Old Generation을 비교적 한가하게 만들 수 있다.
  • Heap 영역의 공간이 작으면 비효율적이다.
  • 대용량 메모리 공간(4GB 이상)이 있는 멀티 프로세서 시스템에서 실행되는 응용 프로그램을 위해 설계
  • G1에는 전통적인 type(Eden, Survior 및 Old Generation) 외에 Humongous Region과 Available / Unused Region이 추가로 존재하며,
    1) Available / Unused Region은 아직 사용되지 않은 영역을 의미하며,
    2) Humongous Region은 단일 객체의 크기가 JVM 실행시 할당된 하나의 Region의 크기에 1/2보다 큰 대용량 객체를 저장하는 영역이다.  
    만일 이와 같은 객체가 존재한다는 것은 애플리케이션 설계의 이슈가 있는지를 확인해야 할 것이다.

 

  • 동작과정
    • 기본적으로 G1 GC는 Young-Only 단계와 Space Reclamation 단계를 반복하면서 수행하는 Cycle 구조로 진행된다. Young Only 단계는 Minor GC만 수행하다가 한정된 Old Generation 비율이 넘으면 Major GC가 수행된다. 그리고 Young Only 단계가 끝날 때까지 두 GC가 혼용된다. Space Reclamation 단계는 Old 영역의 Garbage까지 수집하는 Minor GC 방식의 Mixed GC 방식이 수행된다.

 

  • G1 권장 사용 사례
    • G1의 첫 번째 초점은 GC 지연 시간이 제한된 대규모 힙을 필요로 하는 애플리케이션을 실행하는 사용자에게 솔루션을 제공하는 것입니다.즉, 약 6GB 이상의 힙 크기와 0.5초 미만의 안정적이고 예측 가능한 포즈 시간을 의미합니다. CMS 또는 Parallel Old GC 가비지 콜렉터와 함께 현재 실행 중인 애플리케이션은 다음 특성 중 하나 이상을 가진 경우 G1로 전환하는 것이 좋습니다.
      • 전체 GC 지속 시간이 너무 길거나 너무 자주 발생합니다.
      • 개체 할당률 또는 프로모션 비율은 크게 다릅니다.
      • 불필요한 긴 가비지 수집 또는 압축 일시 중지(0.5~1초 이상)

 

 주의: CMS 또는 Parallel Old GC를 사용하고 있으며 응용 프로그램에서 긴 가비지 수집 일시 중지가 발생하지 않으면 현재 수집기를 그대로 사용해도 됩니다.최신 JDK를 사용하기 위해 G1 컬렉터로 변경할 필요는 없습니다.

 

 Z GC

  • JDK 11부터 실험적으로 도입되었다.
  • 적은 메모리나 큰 메모리에서 STW 시간을 최대한 적게(10ms 이하로) 가져가기 위해 제작되었다.
참고하면 좋을 영상: https://www.youtube.com/watch?v=9so187f-YRM