읽기 전

  • 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
  • 개인적으로 사용해보면서 배운 점을 정리한 글입니다.

문제 제기

자바 가비지 컬렉션은 자바 언어를 공부하면서 꼭 나오는 개념이다. 기본 개념이라 칭하기에는 난이도가 있다. 그러나 GC가 어떻게 동작하는지 이해를 해야 메모리 이슈 등의 문제에 대응할 수 있으니 짚고 넘어갈 필요가 있다.

가비지 컬렉션(Garbage Collection)이란?

C/C++에선 개발자가 객체에 메모리 할당 & 해제를 하였으나 자바에서는 그럴 필요가 없어 포인터의 개념이 존재하지 않는다. 더 이상 사용되지 않는 객체를 자바(JVM)가 스스로 메모리에서 삭제하는 작업을 Garbage Collection(이하 GC)라 부른다. 메모리 중에서도 GC는 "힙(Heap)" 영역에 관여한다.

GC 개요

  • stop-the-world

    GC는 할당되지 않은 객체를 메모리에서 삭제하는 과정을 의미한다. 따라서, 프로그램 실행 중에 GC를 수행한다면 다른 thread의 동작 중 메모리에 변경이 발생하여 안전하게 GC를 수행할 수 없다. 따라서 GC가 수행되기 위해서는 메모리에 어떤 변경이 없음을 보장해야 하므로 JVM이 애플리케이션 실행을 중지시킨다. stop-the-world는 GC를 실행하는 thread를 제외한 모든 thread의 작업이 중지되어 마치 프로그램이 정지된 것처럼 보이는 상태를 의미한다. 모든 GC 알고리즘은 stop-the-world를 동반하며 GC 알고리즘의 성능은 stop-the-world 시간을 줄이는 데 초점이 맞춰져 있다.

  • GC 대상 객체 선정

    GC는 두 가지 전제 조건에 따라 동작한다.

    • 대부분의 객체는 금방 접근 불가능 상태(Unreachable)가 된다.
    • 오래된 객체에서 젊은 객체로의 참조는 드물게 발생한다.

    이 두 가지 전제로 인해 HotSpot VM에서는 힙을 Young 영역과 Old 영역으로 나누었다. 이 영역들을 통칭하여 흔히 자바의 힙 구조라고 부른다. 대부분의 객체는 금방 접근 불가능한 상태로 전환되기에 Young 영역을 두고 오래된 객체들은 젊은 객체보다 오래된 객체들을 참조할 가능성이 높기에 Old 영역으로 몰아넣는다.

    어떤 객체가 Unreachable한 객체인지 판단은 Java Reference에 대한 이해가 필요한데 추후에 정리하려 한다. 우선 GC Root로부터 힙에 존재하는 개체로 참조가 생성되므로 GC Root에 대해 알고 있어야 한다. GC Root가 될 수 있는 항목을 대강 나열하면 아래와 같다.

    1. JVM 메모리의 Stack 영역에 존재하는 참조 변수
    2. Method Area의 static 변수에 의한 참조
    3. JNI에 의해 생성된 객체들에 대한 참조

Development_Java_Garbage_Collection_001

Java Heap의 구조

Development_Java_Garbage_Collection_002

Heap 영역은 Java 7까지는 Special heap 영역으로 Permanent Generation이 존재했으나 Java 8 이후 Metaspace로 대체되면서 2개 영역으로 크게 구분된다.

Young Generation

  • Eden

    • 새로 생성한 대부분의 객체가 저장되는 위치
    • Eden 영역에서 GC가 발생한 뒤 살아남은 객체는 Survivor 영역으로 이동
  • 두 개의 Survivor 영역

    • Eden에서 GC가 발생한 후 생존한 객체들을 담는다.
    • 둘 중 하나는 반드시 비어있어야 한다.
    • Eden과 Old 영역 간의 완충 역할로 Tenuring Threshold값이 초과된 객체는 Old 영역으로 보낸다.

Tenured(=Old) Generation

  • Old
    • Minor GC를 진행하면서 Tenuring Threshold값이 초과된 객체를 보관한다.
    • Old 영역이 가득 차면 Minor GC를 수행할 수 없기 때문에 Major GC가 발생한다.
    • Java 8 부터는 Permanent Gen이 없어지고 Full GC의 의미가 없어져 Major GC = Full GC로 간주하기도 한다.
    • 엄밀히 말하면 Major GC는 Tenured Generation을 청소, Full GC는 Heap 영역 전체를 청소한다고 봐야 한다. 다만 Major GC가 Minor GC에 실패하여 수행되므로 Major GC를 수행한 뒤에는 Minor GC를 수행할 것이다. 따라서 Major GC의 수행은 Full GC와 다를 바 없어 JVM에서도 Major GC를 Full GC 취급한다.

Permanent Generation

Java 7까지 존재했던 영역으로 Method Area의 데이터를 보관했다. 클래스의 메타 데이터, intern된 스트링 Constant Pool, Numeric Constant Pool 등을 보관했었다. Heap 전체 영역을 청소하는 Full GC 발생 시 해당 영역도 GC의 대상이 된다.

Serial GC

가장 기초적인 GC 알고리즘이다. 싱글 thread로 동작하며 live object를 mark한 뒤 특정 영역에 모으고 나머지 객체를 버리는 과정이라 이해할 수 있다. 적은 메모리와 CPU 코어 개수가 적을 때 적합하므로 서버 등 고성능을 요구하는 환경에서는 사용을 지양해야 한다.

Development_Java_Garbage_Collection_003

Minor GC

  • mark and copy : live object를 mark하고 다른 영역으로 copy한 후 기존 영역에 존재하던 object는 GC한다.
  1. 새롭게 생성된 객체는 Eden 영역에 저장된다.

  2. Eden 영역이 Full 상태가 되면 live object를 확인한다. (mark)

  3. 아직 live object라고 마킹된 객체는 Survivor(From) 영역으로 옮긴다.

  4. live obejct가 옮겨진 뒤 Eden 영역에는 Garbage 밖에 남지 않아 Minor GC를 수행한다. 따라서, Eden 영역은 반드시 Empty 상태여야 하며 Survivor(To) 영역 중 하나로 옮겨진 객체들의 Tenuring Threshold 값은 1 증가한다.

Development_Java_Garbage_Collection_004

  1. 다시 객체들이 생성되면서 Eden에 쌓이고 가득 차서 GC가 발생하면 Survivor(From)에 계속 저장한다. 결국 Survivor(From) 영역이 가득 차서 더 이상 GC를 수행할 수 없게 되면 Eden과 Survivor(From) 영역의 live object 리스트를 확인한다. (mark)
  2. 살아있는 객체 확인 작업이 완료되면 Survivor(To) 영역으로 살아있는 객체를 옮긴다.
  3. 옮기는 작업이 끝나면 Eden과 Survivor(From) 영역에는 Garbage만 존재하므로 Minor GC를 수행한다. 옮겨진 객체들의 Tenuring Threshold 값은 1 증가한다. 그리고 From과 To를 전환한다.

Development_Java_Garbage_Collection_005

  1. 또 객체들이 생성되면서 Eden이 가득 차면 Survivor(From) 영역으로 옮기는 GC를 수행하며 결국 Survivor(From) 영역도 가득 차게 된다. GC를 수행하기 위해 live object 탐색을 하던 중 Tenuring Threshold 기준을 충족한 객체가 발견되었으므로 해당 객체를 Old 영역으로 Promotion 시킨다. 물론 Threshold 값을 1 더해준다.
  2. 나머지 live object는 비어있는 Survivor(To) 영역으로 옮긴다. 옮기면서 Threshold 값을 1 더한다.
  3. 옮기는 작업이 끝나면 Eden과 Survivor(From) 영역에는 Garbage만 존재하므로 Minor GC를 수행한 뒤 From과 To를 전환한다.

Development_Java_Garbage_Collection_006

Minor GC가 발생된 이후에는 Eden 영역과 Survivor 영역 중 하나는 완전히 비어있는 상태여야 한다. 그렇지 않으면 뭔가 잘못된 상태임을 의미한다.

참고로 Object Header에 Age를 기록하는 부분의 크기가 6bit이기 때문에 Age의 임계값은 31이다.

Major GC

  • Mark-Sweep-Compact
    • Mark : Old 영역에 살아있는 객체를 식별
    • Sweep : 힙을 순차적으로 확인하여 마킹된 객체만 남김
    • Compaction : 살아남은 객체들이 연속되게 쌓이도록 힙의 앞부분부터 채워넣음

Development_Java_Garbage_Collection_007

Parallel GC

Serial GC와 동작 알고리즘은 동일하나 Serial GC는 GC Thread가 하나인 것에 반해 Parallel GC는 GC Thread가 여러 개이므로 Serial GC보다 빠르게 GC를 처리할 수 있다. 메모리가 충분하고 코어의 개수가 많은 환경에서 유리하다.

Development_Java_Garbage_Collection_008

Parallel Old GC

JDK 5버전에서는 Minor GC와 Major GC가 구분되어 있었으나 JDK 7부터는 기본 Parallel GC 옵션 실행에도 Parallel Old GC도 같이 수행되었다. Yound과 Old 영역이 병렬로 처리된다. 다만, Serial GC와는 달리 Sweep이 아니라 Summary 단계를 수행한다.

  • Mark-Summary-Compact
    • Mark : Old 영역에 살아있는 객체를 식별
    • Summary : 이전에 GC를 수행하여 컴팩션된 영역에 살아있는 객체 식별
    • Compact : 살아남은 객체들이 연속되게 쌓이도록 힙의 앞부분부터 채워넣음

CMS(Concurrent Mark Sweep) GC

Major GC

CMS GC는 Old 영역을 청소하는 Major GC에 대한 알고리즘이다. Young 영역에 대한 GC는 Parallel GC로 수행된다. CMS GC를 구성하는 몇 가지 단계가 있다.

Development_Java_Garbage_Collection_009

  1. Initial Mark : 클래스 로더에서 가장 가까운 객체 중 살아 있는 객체 탐색 후 종료
    • 싱글 Thread로 동작하며 STW Pause가 매우 짧음
  2. Concurrent Mark : Initial Mark 단계에서 살아있다고 보증된 객체가 참조하는 객체들을 따라가면서 확인
    • 다른 Thread가 실행 중인 상태에서 동시에 진행됨
  3. Remark : Concurrent Mark 단계에서 새로 추가되거나 참조가 끊긴 객체를 확인
    • STW Pause 발생하며 Initial Mark 시 발생한 STW Pause보다는 훨씬 길다.
  4. Concurrent Sweep : Remark
    • 다른 Thread가 실행 중인 상태에서 동시에 진행됨

sweep 과정을 다른 Thread와 동시에 진행하기 위해서 Mark에 시간을 투자한다. 따라서, STW Pause 시간이 매우 짧다는 장점이 있다. 모든 애플리케이션의 응답 속도가 매우 중요할 때 사용하는 알고리즘으로 Low Latency GC라고도 부른다.

다만 STW Pause가 짧은 대신 대표적으로 2개의 Trade off가 존재한다.

  1. 다른 GC 알고리즘보다 많은 메모리와 CPU를 요구
  2. Compaction 단계가 존재하지 않음

즉, 고성능 환경이 보장되지 않으면 오히려 속도가 느릴 가능성이 있고 메모리의 파편화가 많아 Compaction 작업 실행 시 다른 GC 알고리즘보다 STW Pause 시간이 길다. 따라서 Compaction 작업을 고려하여 계획을 수립해야 한다.

G1(Garbage First) GC

메모리가 크고 CPU 코어가 많은 환경을 위해 설계되었으며 JDK 9 버전부터는 기본 GC 알고리즘으로 채택되었다. 여타 다른 GC 알고리즘은 Young, Old를 분명하게 구분하여 생각했던 것과는 달리 heap 메모리 영역을 작은 단위의 region으로 분할하여 관리한다.

Development_Java_Garbage_Collection_010

region은 기본적으로 전체 힙 공간을 2048 개로 분할하여 배정된다.

  • Young 영역
    • 멀티 Thread로 GC가 동작하며 살아남은 객체는 Survivor region으로 이동한다.
    • evacuation, compacting 수행
    • 사전에 정의된 aging threshold 값 초과 시 Survivor region의 오래된 객체는 Old region으로 이동한다.
    • Minor GC을 수행할 때마다 Eden, Survivor 영역의 크기는 자동으로 계산되어 정해진다.
  • Old 영역
    • 멀티 Thread로 GC가 동작하며 Major GC가 전체 힙 공간을 대상으로 동작하던 다른 알고리즘과 달리 일부 region에 대해서만 GC를 수행한다.
    • GC를 수행할 region 선택은 region 내 객체를 대상으로 liveness(살아있는 객체 / 사용하는 객체)값을 기준으로 판단한다.
      • liveness가 높은 객체는 재사용될 가능성이 높다고 판단, liveness가 적은 region을 GC한다는 점에서 Garbage First라는 이름이 붙여졌다.

G1 GC의 동작 과정까지 숙지하기엔 아직 과한 감이 있어서 추후에 필자의 지식 수준이 갖춰지면 다루려 한다.

+ Recent posts