본문 바로가기
Java

GC 개념 및 동작 원리

by 긍정의고등어 2022. 9. 13.

지난 글에서 JVM의 구조에 대해 정리해 보았는데 JVM의 구성 요소중 GC에 대해서는 자세한 설명이 빠져있었다. GC만 정리하더라도 글 한 개의 분량이 될 것 같아서 이번 글에 이어서 GC에 대해 정리해본다.

 

Garbage Collection 이란


학부생 때 C언어를 이용한 과제를 진행했을 때 malloc() 함수 사용 후 free() 함수를 실행해서 메모리를 해제해준 기억이 있다. 그 다음 학기에 자바 강의를 수강했는데 자바에서는 별도로 메모리를 관리하지 않아도 되고, GC가 알아서 메모리를 관리해준다고 배웠던 기억이 남아있다.

 

지난 JVM 정리에서 알 수 있듯이 자바를 이용해 개발할 때는 직접 메모리 공간을 할당/반환을 하지 않고, JVM을 통해 할당받고, 반환하게 된다. 이렇게 자바에서는 JVM의 구성요소인 Garbage Collector가 알아서 Heap 영역에서 사용되지 않는 메모리(Garbage)를 관리해주기 때문에 개발자가 별도로 신경을 쓸 필요가 없으며 이 동작을 Garbage Collection 이라고 한다.(System.gc() 메소드를 이용해 강제로 GC를 실행할 수는 있지만 시스템 성능에 영향을 줄 수 있으므로 호출을 권장하지는 않는다.)

 

JVM Heap 구조


VM의 가비지 컬렉터는 Weak Generational Hypothesis 라는 2가지의 전제를 기반으로 설계되었으며 이것들을 바탕으로 Heap을 구조화한다. 2가지 전제는 아래와 같다.

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

정리하자면 객체는 대부분 일회성으로 사용되어 메모리에 오래 남아있는 경우는 드물다는 것이다. 따라서 객체의 생존 기간에 중심을 두어 Heap 영역을 Young, Old 2가지 영역으로 나누어 설계하였다.

그림 1. JVM heap 구조


Young 영역

Young 영역은 내부에서 Eden과 Survival 0, 1로 구분된다. 이름에서 알 수 있듯이 Young 영역은 새롭게 생성된 객체가 할당되는 영역이며, Young 영역에 메모리를 할당하는 것을 Allocation이라고 한다. 이 영역은 앞서 언급한 2가지 전제 중 첫 번째 전제에 기반하여 설계되었으며, 때문에 많은 객체가 Young 영역에 생성되었다가 사라지기를 반복한다.


Old 영역

Old 영역은 Young 영역에서 Reachable 상태를 유지하여 살아남은 객체가 복사되는 영역이며 이것을 Promotion이라고 한다. 보통 수명이 짧은 객체들은 큰 공간을 필요로 하지 않으며, 큰 객체들을 Young 영역이 아니라 바로 Old 영역에 할당되는 경우도 있기 때문에 Old 영역의 크기가 Young 영역의 크기보다 크다.

 

Garbage Collection의 동작 방식


앞서 JVM은 Heap 영역을 크게 두 가지 영역으로 나누었다고 정리했는데, 가비지 컬렉션은 각 두 영역에서 따로 이루어진다. 두 가비지 컬렉션의 동작방식은 세부적으로는 다르지만 기본적으로 2단계의 공통된 단계를 따르는데 그 단계는 아래와 같다.

  • Stop The World
  • Mark and Sweep

Stop The World

Stop The World 는 이름 그대로 가비지 컬렉션을 실행하기 위해 JVM이 애플리케이션의 실행을 멈추는 작업이다. GC 실행 시는 GC 실행을 위한 쓰레드를 제외한 다른 쓰레드의 작업이 전부 중단되고 GC가 끝난 후에 다시 재개된다. 자바 관련된 글 중에서 GC 튜닝 이라는 단어를 찾아볼 수 있는데, 이는 보통 STW의 시간을 줄이는 작업을 뜻하며 JVM 자체에서도 이와 관련된 여러 옵션을 제공하고 있다.


Mark and Sweep

STW를 통해 GC 쓰레드를 제외한 다른 쓰레드의 작업을 중단시키면 GC는 Heap의 Reachable 객체를 스캔하면서 각 객체가 어떤 객체를 참조하고 있는지를 탐색하게 된다. 이후 사용되고 있는(Reachable) 메모리를 식별하는 작업을 한 뒤(Mark) 사용되지 않는것으로 식별된(UnReachable) 객체를 메모리에서 제거한다(Sweep).

그림 2. Reachable 객체 탐색

위 설명에서 사용되고 있는, 사용되고 있지 않은 객체를 각각 Reachable, Unreachable 이라고 언급했는데 그림을 통해 그 이유를 알 수 있다. 앞선 글에서 알아보았듯이 JVM의 내부 메모리에는 Process 단위로 공유되는 Method Area와 Thread 단위로 갖고 있는 Stack, Native Stack이 있다. 이 세 요소를 통틀어 Root Set of References 라고 하며 Heap 영역의 객체를 참조한다. JVM은 Mark를 하기 위해 이 세 영역에서부터 참조하고 있는 객체를 가지를 뻗어나가듯이 탐색하며 체크하고, root로부터 도달할 수 있는 객체(Reachable)를 사용중인 객체로 판별하며 반대의 경우는 사용하지 않는 객체로 판별한다.

 

Minor GC와 Major GC


Minor GC

GC는 Young, Old 영역에서 각각 일어날 수 있는데 Young 영역에서 일어나는 GC를 Minor GC 라고 한다. Minor GC는 Young 영역에 위치한 각각의 영역이 가득 차게 되어 더 이상 새로운 객체를 생성할 수 없는 시점에 발생하며, 마크한 객체를 다른 영역으로 복사하면서 이루어진다. Minor GC에서도 앞서 말한대로 STW가 발생하지만 매우 찰나의 시간이기 때문에 거의 없다고 생각해도 무방하다.

 

Young 영역에서만 일어나는 GC라고는 하나 Young 영역의 객체만 확인한다면 Old 영역 > Young 영역으로 참조하고 있는 객체를 Garbage로 판단하여 삭제할 수도 있다. 여기서 위에서 언급한 2가지 전제 중 두 번째(오래된 객체에서 새로운 객체로의 참조는 아주 적다)를 바탕으로 설계된 Card Table이 사용된다.

그림 3. Card Table

Old 영역의 객체가 Young 영역의 객체를 참조할 경우, 참조 관련 정보를 512 byte 크기의 chunk로 되어 있는 Card Table에 기록해 놓는다. Young 영역에서 Minor GC가 일어날 때 Old 영역의 모든 객체를 스캔하여 Young 영역에 참조하고 있는 객체를 확인하는 것이 아닌, Card Table만을 참조하여 그 여부를 확인한다. Old > Young 으로의 참조관계를 전부 Card Table에 기록하는 것이 오버헤드를 유발할 수 있지만, 앞서 언급한 두 번째 전제를 바탕으로 Card Table에 기록되는 양은 아주 적을것이기 때문에 이는 무시한다.

 

Minor GC 과정을 모식도로 나타내면 아래와 같다.

 

그림 4. Minor GC 1단계

객체가 생성되어 Young 영역의 Eden 영역에 할당된다. 이 때, 객체가 얼마나 오래 살아남았는지를 기록하기 위해 age-bit를 0으로 초기화하여 각 객체에 할당하며, 이 값은 Minor GC에서 살아남을 때마다 1씩 증가한다.

그림 5. Minor GC 2단계

시간이 지나서 Young > Eden 영역에 공간이 남지 않게 되면 Minor GC가 일어나게 된다. Eden의 객체 중 Reachable한 객체만 살아남아 Survival 0 영역으로 이동되고 나머지는 회수된다(이 과정에서 살아남은 객체는 age-bit가 1 증가한다).

그림 6. Minor GC 3단계

Minor GC가 일어난 이후에 Eden 영역이 꽉 차게 되고 다시 Minor GC가 일어나 비어있는 Survival 1 영역으로 Eden + Survival 0 영역의 Reachable 한 객체가 이동된다.

그림 7. Minor GC 4단계

다시 Eden 영역이 가득차서 Minor GC가 일어나고 이번에는 Survival 0 영역이 비었기 때문에 Eden + Survival 1 영역의 Reachable 한 객체가 이동되며, 이 과정이 계속해서 반복된다.

그림 8. Minor GC 5단계

이러한 과정이 반복되면 특정 객체들의 age-bit가 JVM의 설정값 이상이 되는 경우가 발행하는데, 이 객체들은 오래 쓰일 객체로 판단되며 Old 영역으로 이동된다(Promotion).

그림 9. Major GC 동작 단계

이러한 과정이 다시 반복되어 결국 Old 영역이 전부 차게 되면 Major GC가 일어나게 된다.


Major GC

Young 영역에서 Minor GC가 계속되어 Old 영역이 가득 찼을 때 실행되며, Minor GC에 비해 실행 속도가 몇 배는 걸리는 작업이다. 기본적으로 앞서 언급한 Stop The World, Mark and Sweep 알고리즘을 바탕으로 수행되며, JVM에서 관련한 여러 옵션을 제공한다.

 

참고


댓글