Language/Java

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

olsohee 2023. 12. 19. 18:38

가비지 컬렉션

가비지 컬렉션은 자바의 메모리 관리 방법 중 하나로, JVM의 Heap 영역에 동적으로 할당했던 메모리 중 더이상 사용되지 않는 메모리 객체를 모아 주기적으로 제거하는 프로세스를 말한다. C/C++ 언어의 경우 이런 가비지 컬렉션이 없기 때문에 개발자가 수동으로 메모리 관리를 해주어야 한다. 그러나 자바에서는 가비지 컬렉터가 대신 메모리 관리를 해주기 때문에 개발자는 메모리 관리나 메모리 누수(필요하지 않은 메모리를 계속 점유하고 있는 현상)에 신경쓰지 않아도 된다는 장점이 있다.

 

[Stop The World, STW]

그러나 이러한 가비지 컬렉션에도 단점이 있다. 자동으로 처리해준다 해도 메모리가 언제 해제되는지 알 수 없어 제어하기 힘들며, 가비지 컬렉션이 동작하는 동안 다른 동작을 멈추기 때문에 오버헤드(어떤 처리를 하기 위해 들어가는 간접적인 처리 시간/메모리)가 발생한다. 이를 STW(Stop The World)라고 한다.

 

STW는 가비지 컬렉션을 실행하기 위해 JVM이 프로그램 실행을 멈추는 현상을 의미한다. 가비지 컬렉션이 작동하는 동안 가비지 컬렉션 관련 스레드를 제외한 모든 스레드는 멈추게 된다. 이로 인해 가비지 컬렉션이 너무 자주 실행되면 소프트웨어 성능 하락의 원인이 되기도 한다. 따라서 가비지 컬렉션의 시간을 최소화시키는 것이 중요하고, 이러한 최적화 작업을 GC 튜닝이라고 한다.

가비지 컬렉션의 대상

가비지 컬렉션은 특정 객체가 garbage인지 아닌지 판단하기 위해서 도달성, 도달 능력이라는 개념을 적용한다. 해당 객체를 참조하는 객체가 있으면 Reachable로 구분되고, 해당 객체를 참조하는 객체가 없으면 Unreachable로 구분하여 가비지 컬렉션의 대상이 된다.

JVM의 Runtime Data Area에서 실제 객체들은 힙 영역에 생성되고, 메소드 영역이나 스택 영역에 생성된 객체들은 힙 영역의 객체를 참조한다. 하지만 메소드가 끝나는 등의 특정 이벤트들로 인하여 힙 영역의 객체를 참조하는 참조 변수가 삭제되면, 위 그림의 빨간색 객체와 같이 어디서도 참조하고 있지 않은 객체(Unreachable)가 발생하게 된다. 가비지 컬렉터는 이러한 객체들을 주기적으로 제거해준다.

가비지 컬렉션의 동작 과정

Mark And Sweep

그렇다면 가비지 컬렉터가 어떻게 Unreachable한 객체를 청소하는지 알아보자.  Mark And Sweep은 가비지 컬렉션이 동작하는 기초적인 청소 과정이다.

  1. Mark: Root Space로부터 그래프 순회를 통해 연결된 객체들을 찾아내어 각각 어떤 객체들에 의해 참조되는지 마킹한다.
  2. Sweep: 참조되지 않는 객체, 즉 Unreachable 객체들을 힙 영역에서 제거한다.
  3. Compact: Sweep 후에 분산된 객체들을 힙의 시작 주소로 모아 메모리가 할당된 부분과 할당되지 않은 부분을 구분한다.

[Root Space]

Mark And Sweep 방식은 루트로부터 해당 객체에 접근이 가능한지가 메모리 해제의 기준이 된다. JVM GC에서 Root Space는 힙 메모리 영역을 참조하는 메소드 영역, 스택 영역, 네이티브 메소드 스택이 된다. 즉 이들을 시작으로 이들이 참조하는 힙 영역의 객체들을 순회한다.

Heap 영역의 메모리 구조

힙 영역은 처음 설계될 때 객체는 대부분 일회성이며, 메모리에 오래 남아있는 경우는 드물다는 것을 전제로 설계되었다. 따라서 이러한 특성을 이용하여 효율적인 메모리 관리를 위해 힙 영역은 Young과 Old로, 2가지 영역으로 나누어 설계되었다. 그리고 Young 영역은 또 다시 Eden, Survivor 0, Survivor 1로, 3가지 영역으로 나뉜다.

  • Young 영역(Young Generation)
    • 새롭게 생성된 객체가 할당되는 영역이다. 대부분의 객체가 금방 Unreachable 한 상태가 되어 가비지 컬렉션의 대상이 되기 때문에 많은 객체가 처음에는 Young 영역에 생성되었다가 사라진다.
    • Young 영역은 Old 영역에 비해 상대적으로 작기 때문에 가비지 컬렉션을 통해 제거할 객체를 찾아 제거하는데 적은 시간이 걸린다.
    • 이 때문에 Young 영역에서 발생되는 가비지 컬렉션을 Minor GC라고 한다.
    • Eden
      • new 연산자를 통해 생성된 객체가 위치하는 영역이다.
      • 정기적인 가비지 컬렉션을 통해 살아남은 Reachable 객체들은 Eden에서 Survivor 영역으로 보내진다.
    • Survival 0, Survival 1
      • 최소 1번의 가비지 컬렉션을 통해 살아남은 객체가 존재하는 영역이다.
      • 이때 Survivor 0 또는 Survivor 1 둘 중 하나는 꼭 비어 있어야 하고, 하나는 꼭 사용되어야 한다.
  • Old 영역(Old Generation)
    • Young 영역에서 Reachable 한 상태를 유지하며 살아남은 객체가 복사되는 영역이다.
    • Young 영역보다 더 큰 메모리 공간이 할당된다. 그 이유는 Young 영역의 객체들은 빈번하게 가비지 컬렉션이 발생하여 메모리가 해제되기 때문에 큰 공간을 필요로 하지 않는다. 반면 Old 영역의 객체들은 수명이 더 길어 객체들이 쌓이게 되기 때문이다.
    • Old 영역에 대한 가비지 컬렉션을 Major GC 또는 Full GC라고 한다.

Minor GC 과정

처음 생성된 객체는 Young 영역 중 Eden 영역에 생성된다.

객체가 계속 생성되어 Eden 영역이 꽉 차면 Minor GC가 발생한다.

Mark 동작을 통해 Reachable 객체를 탐색한다.

Eden 영역에서 살아남은 객체는 Survivor 영역으로 이동한다.

가비지 컬렉션의 대상인 Unreachable 객체의 메모리를 해제한다(Sweep). 

살아남은 모든 객체들은 age 값을 1씩 증가시킨다. 이때 age 값이란, Survivor 영역에서 객체가 살아남은 횟수를 의미하는 값이다. 만약 age 값이 임계값에 다다르면 Old 영역으로 이동하는 Promotion 여부를 결정한다. JVM 중 가장 일반적인 HotSpot JVM의 경우에는 이 age 임계값이 31이다. 객체의 헤더에 age 값을 기록하는 부분이 6비트이기 때문이다.

또 다시 Eden 영역에 객체들이 가득 차면 Minor GC가 발생하고 mark 한다. 이때 Survivor 영역도 Young 영역으로, Minor GC의 대상이다.

Mark 한 객체들을 비어있는 Survivor로 이동하고 가비지 컬렉션의 대상이 되는 객체들은 Sweep한다.

다시 살아남은 객체들은 age 값을 각각 1씩 증가시키고, 이러한 과정이 반복된다.

Major GC 과정(Full GC)

Old 영역은 오래 살아남은 객체들이 존재하는 공간이다. Young 영역에 존재하는 객체들 중 age 값이 임계값까지 도달했음에도 가비지 컬렉션에 의해 제거되지 않은 객체들은 Old 영역으로 이동된다. 그리고 객체들이 계속 Promotion되어 Old 영역의 메모리가 가득 차면 Major GC가 발생한다.

 

age 임계값이 8이라고 가정했을 때, 다음과 같이 특정 객체의 age 값이 임계값에 도달하게 되면, 이 객체들은 Old 영역으로 이동된다. 이를 Promotion이라고 한다. 

위 과정이 반복되며 Old 영역이 가득 차게 되면 Major GC가 발생한다. 

Major GC는 Old 영역에 위치한 객체들을 Mark And Sweep하여 참조되지 않는 객체들을 삭제하는 과정이다. 그런데 Old 영역은 Young 영역에 비해 상대적으로 큰 공간을 가지기 때문에 Minor GC보다 Major GC에 더 많은 시간이 걸린다. 바로 여기서 Stop The World 문제가 발생한다. Major GC가 발생하면 스레드가 멈추고 Old 영역에 대한 Mark And Sweep 작업이 일어나기 때문이다. 따라서 이 문제를 해결하기 위해 다양한 가비지 컬렉션 알고리즘이 등장하게 되었다.

가비지 컬렉션 알고리즘 종류

JVM이 가비지 컬렉션을 통해 메모리를 자동으로 관리해준다는 것은 장점이지만, 가비지 컬렉션을 수행하는 과정에서 발생하는 Stop The World는 애플리케이션의 지연을 일으킨다는 문제가 있다. 또한 자바가 발전됨에 따라 힙 영역의 사이즈가 커지면서 애플리케이션의 지연 현상이 두드러지게 되었고, 이를 해결하기 위해 다양한 가비지 컬렉션 알고리즘들이 등장하게 되었다. 참고로 앞으로 소개되는 알고리즘은 모두 설정을 통해 자바에 적용할 수 있다. 즉, 상황에 따라 적절한 GC 알고리즘은 사용할 수 있다.

Serial GC

  • 서버의 CPU 코어가 1개일 때 사용하기 위해 개발된 가장 단순한 GC이다.
  • GC를 처리하는 쓰레드가 1개(싱글 쓰레드)라서 가장 Stop The World 시간이 길다.
  • Minor GC 에는 Mark-Sweep을 사용하고, Major GC에는 Mark-Sweep-Compact를 사용한다.
  • 보통 실무에서 사용하는 경우는 없다. (디바이스 성능이 안좋아서 CPU 코어가 1개인 경우에만 사용) 

Parallel GC

  • Java 8의 디폴트 GC이다.
  • Serial GC와 기본적인 알고리즘은 같지만, Young 영역의 Minor GC를 멀티 스레드로 수행한다. (Old 영역은 여전히 싱글 스레드이다.)
  • Serial GC에 비해 Stop The World 시간이 감소한다.

Parallel Old GC(Parallel Compacting Collector)

  • Parallel GC를 개선한 버전이다.
  • Young 영역뿐만 아니라, Old 영역에서도 멀티 스레드로 수행한다.
  • 새로운 가비지 컬렉션 청소 방식인 Mark-Summary-Compact 방식을 사용한다.

이 외의 가비지 컬렉션 알고리즘은 다음을 참고하자.


Reference