개요

GC, 가비지 컬렉션은 메모리 관리 기법 중 하나이다.

실제 C 에서 동적할당하기 위해서는 malloc 을 사용하며 개발자는 free 메서드를 통해 명시적으로 메모리를 꼭 해제해주어야한다. 그 이유는 C 언어에 Garbage Collector 가 없기 때문이다.

그럼 Java 에서 메모리를 할당하고 해제한 경험이 있는가? 아마 대부분은 없을 것이다. 이는 Java 에서 GC 가 존재하기에 언어 자체적으로 메모리 누수를 막아주기 때문이다.

그렇다면 Java 에서는 메모리 누수에 대해 걱정할 필요가 없는가??

GC

GC 알고리즘

Reference Counting

image

루트 스페이스에 대해 먼저 설명하자면 이는 스택 변수, 전역 변수 등 heap 영역 참조를 담은 변수이다. 실제 heap 공간에 존재하는 변수들을 참조하는 변수라는 뜻이다. 모든 heap 변수들은 루트 스페이스와 이어져있어야 실제 참조가 가능하다. 그 뜻은 GC 는 루트 스페이스에서 연결되지 않은 heap 객체들을 삭제할 필요가 있다는 뜻이다.

Reference Counting 은 GC 를 진행하는 실제 알고리즘 중 하나이다. 각 객체들은 가장 앞단에 해당 객체가 몇 가지 방법으로 참조될 수 있는 지 기록한 int 값을 가지고 있다. 해당 값이 0 이라면 참조될 수 있는 방법이 존재하지 않기에 삭제할 수 있다.

하지만 이 방법에서는 순환 참조를 구별해내지 못한다. 만약 루트 스페이스에서 참조할 수 없지만 두 객체가 서로를 참조하고 있다면 각 int 값은 1로 삭제되지 않을 것이다. 이는 분명히 삭제되어야할 객체를 삭제하지 못하는 허점이다.

Mark and Sweep

image

다음 방법으로는 Mark and Sweep 가 존재한다.

우선 GC 는 루트 스페이스로부터 접근 가능한 객체와 불가능한 객체를 구분해낸다. 이후 실제 접근 가능한 객체만을 남기고 나머지 객체는 삭제한다.

추가적으로 객체들을 Heap 영역에서 Compaction 도 가능하다. 이를 통해 메모리 파편화도 방지할 수 있다.

JVM 의 GC

실제 JVM 에서 GC 동작에 대해서 알아보자.

image

위 그림이 실제 Heap 영역을 사용하는 방식이다.

Heap 영역은 크게 Young Generation, Old Generation 으로 나뉜다.

Young Generation

Young Generation (이하 YG) 는 또 3가지 영역으로 나뉜다.

  1. Eden
    • 새로운 객체가 할당되는 곳
    • 해당 영역이 가득차면 minor GC 를 실행하게 되고 Unreachable 객체들을 삭제하고 Reachable 객체들을 Survival 로 옮긴다.
  2. Survival 0
    • Survival 영역은 0 과 1 중 한 곳은 반드시 비어있어야한다.
    • 또한 해당 영역에 존재하는 객체들은 age-bit 를 가지고 있다.
  3. Survival 1
    • minor GC 가 다시 일어나면 Reachable 객체들과 기존 Survival 0 에 존재하는 객체들을 Survival 1 로 이동시킨다.
    • 이때 age-bit 를 +1 한다.
    • 그러면 Survival 0 은 비어있는 상태이고 minor GC 가 일어날 때 해당 작업을 반복한다.

정리하자면 minor GC 는 Eden 에 새로운 객체들로 가득 찼을 때 실행되며 Reachable 객체들을 기존 Survival 에 존재하는 객체들과 함께 다른 Survival 로 옮긴다.

그리고 유지하던 age-bit 는 일정 수준이 넘어가면 해당 객체를 Old Generation 으로 이동시키는 역할을 한다. 이를 Promotion 이라 칭한다.

실제 Java 8 에서 parallel GC 기준으로 age-bit 의 수준은 15로 설정되어있다.

Old Generation

age-bit 를 통해 Old Generation 으로 넘어온 객체들이 가득 차면 여기서는 major GC 가 발생한다. 이때 major GC 는 minor 보다 훨씬 많은 시간이 소요된다.

이렇게 하는 이유가 뭘까?

대부분의 객체들은 수명이 짧기 때문이다.

항상 모든 객체들을 GC 하는 것이 아니라 수명이 짧은 객체와 수명이 긴 객체를 나누어 GC 를 실행시켜 성능을 높이고자 하였다.

Stop The World

GC 를 실행하기 위해서는 JVM 이 어플리케이션의 실행을 멈춰야한다. 이를 Stop The World 라 표현하며 이는 성능에 직결되는 문제이다. Stop 시간이 1초를 넘어간다면 DB 커넥션에 문제가 생길 수 있고 GC 가 직접적인 에러의 원인이 될 수 있다.

Serial GC

image

하나의 스레드로 GC 를 실행하는 방법이다. Stop 시간이 길 수 밖에 없다.

싱글 스레드 환경이나 Heap 이 매우 작을 때 사용할 수 있다.

Parallel GC

image

딱 봐도 여러 개의 스레드로 GC 를 실행하며 짧은 Stop 시간을 가진다.

실제 Java 8 의 default GC 방식이다.

CMS GC

image

Stop 시간을 최소화하기 위한 고민이 담겨있다. Mark 후에 대부분의 GC 작업을 Application 과 함께 진행한다는 특징이 있다.

하지만 CPU 와 메모리 등의 자원 사용량이 높고 Mark and Sweep 과정 이후 Compaction 이 기본적으로 제공되지 않는다.

G1 GC

image

Garbage First 라는 뜻이다. Heap 을 일정 크기로 잘라서 영역들을 YG, OG 로 사용하는 방법이다.

런타임에 GC 가 YG, OG 의 영역 개수를 튜닝함으로써 가장 적은 Stop 시간을 가질 수 있었다.

Java 9 이상부터는 G1 GC 를 Deault GC 로 가진다.

추가

Java 의 이면에서 GC 는 누수된 메모리를 열심히 제거해주고 있었다. 실제 성능을 우선시한다면 직접 메모리 관리를 하거나 GC 튜닝도 가능하다.

실제 GC 튜닝은 Heap 의 크기, YG와 OG 의 비율, GC 실행 방식 등을 설정할 수 있음을 참고해두자.

참고

[10분 테코톡] 🤔 조엘의 GC

업데이트:

댓글남기기