JVM GC 밑바닥 핥아보기 1부

KIYOUNG KWON·2024년 11월 10일
0

개요

JVM 밑바닥까지 파헤치기 라는 책을 읽어보면서 GC(Garbage Collector) 부분을 정리해보려고 한다. 이 책은 GC 말고도 JVM에 대한 매우 자세한 내용을 다루고 있지만 평소 궁금하던 GC에 관련된 내용을 정리 및 공유하는 글을 작성하여 나 자신의 JVM GC에 대한 이해도를 높여보려고 한다.

해당 글은 책의 2부 3장을 정리한 내용이다.

JVM 메모리 구조


위 그림은 JVM 런타임의 데이터 영역을 표현한 그림이다. 우리가 여기서 봐야할 부분은 Class(Method) Area와 Heap이다. Stack의 경우 메서드의 호출 및 반환에 따라 생명주기가 명확하다. 그러면 동적으로 변수가 할당되는 Heap영역과 클래스의 Metadata를 관리하는 메서드 영역(Method Area)이 Garbage Collector가 일하는 대상이 될 것 이다.

메서드 영역의 경우 class metadata를 관리하는데 class에서 사용하는 상수풀과 static 변수들이 이에 해당된다. JVM의 GC에서 더 이상 class를 사용하지 않는다 판단할 때 class의 metadata와 함께 제거 된다.

Garbage Collection 알고리즘

Garbage Collection에서 중요한 부분은 무엇일까요? Garbage Collection을 처음 적용한 언어인 리스프를 개발한 존 맥카시는 Garbage Collection이 해결해야할 문제 3가지를 아래와 같이 정의하였다.

  • 어떤 메모리를 회수해야 할까?
  • 언제 회수해야 할까?
  • 어떻게 회수해야 할까?

결국 Garbage Collector는 어떤 객체가 더 이상 사용되지 않는지 판단하고 이를 어느 시점에 어떻게 회수할지 결정하는 프로그램이라고 보면 될 것 이다. 여기서 중요한 2가지 키워드가 있다.

  • 참조 가능성 분석 알고리즘
    참조 가능성 분석 알고리즘은 GC Root 집합에서 시작하여 객체를 따라가며 도달 가능한(즉 아직 사용중인) 모든 객체를 탐색한다. 객체가 참조되고 있는지를 그래프 형식으로 나타내고 탐색하여, 연결된 객체들을 확인 후 도달 가능하지 않은(사용하지 않는) 객체를 수집하고 차 후 해제한다. 여기서 GC Root는 탐색을 하는 시작점으로 가상 머신의 스택, 정적 필드가 참조하는 객체, 메서드 영역에서 상수로 참조되는 객체 등이 후보가 될 수 있다.

  • 세대 단위 이론
    현재 상용 가상 머신들이 채택한 가비지 컬렉터는 대부분 세대 단위 컬렉션 이론에 기초해 설계되었다. 아래의 2가지 가설이 합쳐져 현존하는 가비지 컬렉터들의 설계 원칙이 되었다. 즉 Garbage Collection을 신세대와 구세대로 나누어 전체를 스캔하기 보단 주로 신세대를 스캔하여 GC를 수행(마이너 GC)하고 메모리가 너무 부족하게 되면 구세대를 포함하여 GC를 수행(메이져 GC)하도록 한다.

  1. 약한 세대 가설: 대다수 객체는 일찍 죽는다.
  2. 강한 세대 가설: 가비지 컬렉션 과정에서 살아남은 횟수가 늘어날수록 더 오래 살 가능성이 커진다.

마크 스윕 알고리즘


이제 참조 가능성 분석 알고리즘을 베이스로 생존할 객체와 회수할 객체가 판명되었다고 가정해보자. 그러면 이제 Garbage Collector는 메모리를 회수를 해야 할 것이다. 그러면 어떻게 회수를 해야할까?

가장 처음으로 나온 콜렉션 알로리즘이 마크 스윕 알고리즘이다. 위 그림에서 보이는 바와 같이 회수 대상을 표시(마크)하고 회수(스윕) 한다. 단순해서 빠르지만 몇가지 문제가 있다.

  • 실행 효율이 일정하지 않음, 회수할 객체가 많아질수록 효율이 떨어짐
  • 메모리 파편화가 심함

마크 카피 알고리즘


마크 스윕 알고리즘의 단점을 보완하기 위해 메모리 영역을 2개로 나누어 생존할 객체만 한 곳으로 복사한뒤 기존에 사용하던 영역을 전부 회수해버리면 된다. 대다수의 객체가 살아남으면 복사에 필요한 시간이 늘어나 효율이 나빠지긴 하지만 대다수가 회수 된다면 아주 좋은 효율을 보여주고 메모리 파편화 문제도 해결된다.

즉 약한 세대 가설을 기반으로 대다수의 객체는 일찍 죽는다는 가정을 기준으로 한다면 신세대에 한정해서 해당 알고리즘을 사용하면 효율적일 것이다. 실제로 대다수의 가상 머신들이 신세대에 해당 알고리즘을 활용한다.

마크 컴팩트 알고리즘


앞서 이야기한 마크 카피 알고리즘은 대부분이 살아남을 수 있는 구세대에선 적합하지 않다. 그에 따라 구세대에 적용하기 위한 마크 컴팩트 알고리즘이 생겨났다. 마크 스윕과 결정적인 차이는 회수 후 메모리의 이동이 발생한다는 것이다. 다만 구세대의 특성 상 이동 시킬 객체가 상당히 많을 것이고 참조를 갱신하는 동안 어플리케이션을 멈출 수 밖에 없을 것이다.

흔히 스탑 더 월드(stop the world)라는 표현을 사용하는데 결국 참조를 갱신하기 때문에 발생하게 된다. 어플리케이션에 따라 다르겠지만 처리량이 중요한 서버 어플리케이션의 경우 이러한 스탑 더 월드가 길어지는 것은 고객 경험에 치명적일 것이다. 그래서 보통은 마크 스윕으로 빠르게 처리를 하고 메모리 파편화가 너무 심해져 더 이상 할당이 힘들어지게 되면 마크 컴팩트 알고리즘을 사용하게 된다.

이 처럼 각 회수 알고리즘은 최신의 알고리즘이 더 좋다기 보단 상황에 맞게 잘 적용하여 서로의 단점을 보완하는 방향으로 발전해왔다.

세부구현에 필요한 알고리즘

Garbage Collector의 종류를 알아보기 전에 이를 이해하는데 필요한 몇가지 기반 지식을 알아보자.

루트 노드 열거

루트 노드 열거란 GC루트 집합에서 참조 체인을 찾는 작업이다. 해당 과정은 특정 시점에서 고정된 상태에서 진행되어야 하기에 사용자 스레드를 일시 정지 해야한다. 따라서 스톱 더 월드를 피할 수 없다. 다만 핫스팟 JVM의 경우 실행 콘텍스트와 전역 참조의 위치를 기록하는 OopMap이라는 데이터 구조를 활용하여 메모리를 전부 스캔하지 않고 루트가 될 수 있는 부분을 Jit 컴파일 과정에서 미리 기록하여 효율을 올린다.

안전지점

모든 명령어에 대해 OopMap을 만들면 메모리를 매우 비효율적 일 것이다. 그 대신 안전지점이라고 하는 특정한 위치에서만 OopMap을 기록한다. Garbage Collector는 안전지점에 도달하기 전까진 절대 사용자 스레드를 멈춰세우지 않는다. 안전지점은 보통 메서드 호출, 순환문, 예외 처리 등 명령어 흐름이 다중화 되는 명령어와 객체 생성 명령어에서 생성되며 핫스팟 JVM의 경우 사용자 스레드에서 폴링하는 방식으로 안전지점을 인지한다.

안전지역

안전 지역은 스레드가 일시적으로 GC에 의해 멈춰질 수 있는 코드 영역이다. 특정 스레드가 어떤 코드 영역에 진입해 있을 때, 그 스레드가 실행되는 동안 참조 관계를 변경하지 않는다는 것을 보장하는 것이다. 해당 영역에선 GC가 언제든 실행되어도 상관없지만 GC가 실행되고 나면 안전지역을 벗어나기 전에 GC가 완료되었는지 확인해야한다.

기억 집합과 카드 테이블

기억 집합과 카드 테이블은 구세대에서 참조하는 신세대를 빠르게 확인하여 GC Root에 포함시키기 위해 사용한다.

  • 기억 집합 (Remembered Set)
    GC가 신세대 영역을 수집할 때, 구세대 영역에서 신세대 영역을 참조하는 객체를 추적하기 위해 사용된다. 이를 통해 GC는 구세대 영역의 모든 객체를 검사하지 않고도 효율적으로 참조 관계를 파악할 수 있다.

  • 카드 테이블 (Card Table)
    힙을 카드라고 불리는 작은 단위로 나누고, 각 카드에 세대간 참조가 있는지를 표시하는 배열이다. 세대간 참조가 존재하는 경우 해당 배열에 참조가 존재함을 표기(dirty)하고 차 후 루트 노드 열거 시점에 해당 카드에 존재하는 객체를 GC 루트에 포함시킵니다.

쓰기장벽

쓰기 장벽은 객체의 참조 관계가 변경될 때마다 해당 변화를 기록하는 메커니즘이다. 객체 간의 참조가 변경되면 GC는 이 변경을 추적해 기억 집합이나 카드 테이블에 반영해야 하는데, 쓰기 장벽을 통해 이러한 변경 사항을 감지한다.

동시 접근 가능성 분석

동시 접근 가능성 분석은 GC가 애플리케이션 실행 중에도 객체의 참조 가능성을 동시에 표기(marking)하는 과정이다. 이는 애플리케이션의 일시 중지를 최소화하여 응답성을 높이기 위한 GC 최적화 기법으로 CMS(Concurrent Mark-Sweep) GC나 G1 GC처럼 저지연(low-latency) GC에서 주로 사용된다.

다음 글에선

여기까지 JVM에 포함되어 있는 Garbage Collector를 이해하기 위한 기반 지식에 대해 알아보았다. 다음 글에선 JVM에서 사용된 아래의 Garbage Collector들에 대해서 자세히 알아보고 적합한 컬렉터를 선택하는 방법에 대해서 알아보려고 한다.

  • 시리얼 컬렉터
  • 파뉴 컬렉터
  • 페러렐 스캐빈저 컬렉터
  • 시리얼 올드 컬렉터
  • 페러렐 올드 컬렉터
  • CMS 컬렉터
  • G1 컬렉터
  • 세넌도어
  • ZGC

0개의 댓글