JVM 밑바닥까지 파헤치기 - GC

유승선 ·2025년 2월 12일
0

자바 독학

목록 보기
11/16
post-thumbnail

JVM 을 깊게 알고싶어서 읽기 시작한 책. 어려운 내용이 더 많지만 최대한 학습해보려고 합니다.


시리얼 컬렉터

시리얼 컬렉터는 싱글 스레드로 GC 가 실행이 되던 초기 모델이다. 이때 중요한건 하나의 GC 스레드가 작업을 모두 처리한다는 의미보다는 가비지 컬렉션이 시작되면 '회수가 완료 될 때까지 다른 모든 작업 스레드가 멈춰 있어야 한다' 는 점이다. 그리고 이것을 'Stop the World' 혹은 STW 라고 부른다. 이 시간이 길어진다면 마치 사용자의 컴퓨터가 한시간 마다 5분씩 멈추는 경험과 유사할 것이다. 이런 불편함을 인지하고 있기에 JDK 1.3 부터 JVM 은 사용자 스레드가 멈추는 STW 시간을 최소화 하기 위해서 진화해왔다. 시리얼 컬렉터는 초기 모델이기 때문에 싱글 스레드를 사용하지만 사실 간단하고 효율적이라는 이점도 있다. 가용 메모리가 적은 환경에서는, 단일 코어 프로세서 또는 코어 수가 적은 환경이라면 싱글 스레드로 인한 GC 는 오버헤드 없이 실행 될 수 있다. -XX:+UseSerialGC 매개 변수를 추가해서 사용 가능하다.

ParNew GC

파뉴 컬렉터 는 여러 스레드를 활용하여 시리얼 컬렉터를 병렬화한 버전이다. 정확히는 멀티 스레드를 활용하여 minor gc 를 발생시키고 Young Generation (신세대) 를 관리하는 GC 라고 할 수 있다. 시리얼 컬렉터와 비교해서 나아진점이 딱히 없다고 적혀있지만 JDK 7 전까지는 신세대용 컬렉터로 인기가 많았다. 이유는 바로 CMS 컬렉터와 조합하여 사용할 수 있는 유일한 컬렉터였기 때문이다. 신세대용 CMS 를 활성화 하면은 기본적으로 파뉴가 선택 되었고 G1 등장 전에는 입지가 탄탄했다. G1 의 등장 이후로는 힙 전체를 대상으로 GC 가 가능했기 때문에 파뉴 컬렉터도 역사속으로 사라졌다. 다른 컬렉터와의 조합은 불가능했기 때문에 CMS 가 종료 되면서 파뉴도 사라진 것이라 볼 수 있다.

Parallel GC

Parallel 컬렉터는 자바 8에서 기본으로 사용이 되는 GC다. 이 GC 도 이 전에 파뉴 컬렉터와 비슷하게 여러 스레드를 사용해 병렬로 실행한다는 특징을 공유하지만, 이 GC 만의 특별한 점은 다른 컬렉터와 다르게 '처리량' 을 제어하는게 목표이다. 멀티코어를 활용하여 병렬로 Yong Generation 을 정리하는 패러렐 스캔비지 컬렉터, 그리고 Old Generation 을 정리하는 패러렐 올드 컬렉터가 조합되어서 현재의 Parallel GC 가 되었다. 가비지 컬렉터 정지 시간의 최대값을 지정할 수 있는 -XX:MaxGCPauseMills 옵션을 활용해 일정 수준의 GC 멈춤 시간을 유지 가능하다. 하지만 너무 낮게 설정한다면 그만큼 처리량도 낮아진다. 즉, 이 값을 줄이면 시스템은 Young Generation 메모리르 더 작게 조절한다. 간단하게 500MB 메모리를 회수하는 시간보다 300MB 를 회수하는것이 더 빠르기 때문이다. 정지 시간은 줄었지만 처리량도 줄어드는 트레이드 오프가 존재한다.

책에서는 언급해둘만한 매게 변수로 -XX:+UseAdaptiveSizePolicy 라는 JVM 의 힙 메모리 크기 및 GC 관련 파라미터를 자동 조정하는 기능을 활성화 하는 옵션도 있다. 기본적으로 활성화가 되어있으며 GC 의 효율성과 성능을 최대한 유지하면서 힙 메모리 크기를 동적으로 조절한다.

GC 튜닝을 통해 응답 속도를 최적화 하고 싶다면 MaxGCPauseMillis (최대 GC 중단 시간) 같은 타임을 조절해서 고려할 수 있다.

CMS GC

CMS GC 는 현재의 자바 어플리케이션이 목표한 STW 를 최소로 줄이고 유저 스레드와 동시에 실행이 되는 최초의 모델이라고도 할 수 있다. 그리고 CMS 의 동작 방식은 아래 4 스텝으로 나뉜다.

  1. 최초 표시
  2. 동시 표시
  3. 재표시
  4. 동시 쓸기

최초표시 단계에서는 GC 루트와 직접 연결된 객체들만 표시하는 작업 (STW)
동시 표시 단계는 GC 루트와 직접 연결된 객체들로부터 시작해 객체 그래프 전체를 탐색
재표시 단계에서는 동시 표시 도중 참조 관계를 변경한 객체들을 바로 잡는다. (STW)
동시 쓸기 단계에서는 앞의 세 가지 표시 단계에서 죽었다고 판단한 객체들을 쓸어 담는다. 살아 있는 객체는 옮길 필요가 없기 때문에 이 단계 역시 사용자 스레드를 멈추지 않고 실행한다.

CMS GC 는 첫번째 성공작이라 할 수 있지만 대표적인 단점들을 가지고 있었기에 완벽하지는 않았다.

첫재, CMS 는 프로세서 자원에 아주 민감하다, 동시 수행 단계에서 만은 자원을 소비하기 때문에 프로그램에 미치는 여향이 크다.

둘째, 마크-스윕 알고리즘을 사용하고 회수 작업 끝에 상당한 메모리 파편화를 만든다. 이것은 곧 큰 객체를 할당할 때 문제를 발생시킨다 (파편화 때문에 연속된 공간을 찾지 못하기 때문) 이런 이유로 파편화가 심해지면 전체 GC 를 수행해야 한다. 이 문제를 해결하고자 기타 제공하는 옵션이 있지만 여전히 완벽한 해결책은 되지 못했다.

G1GC (가비지 우선 컬렉터)

G1 은 주로 서버용 애플리케이션에 집중한 컬렉터다. 자바9부터 Parallel GC 를 밀어내고 기본 GCrk ehldjTek. CMS 의 대체제이자 후게자를 목표로, G1의 설계자들은 정지 시간 예측 모델을 만들고자 했다. 정지시간 예측 모델이란? 목표 시간을 M 밀리초로 설정하면 가비지 컬렉터가 쓰는 시간이 M 밀리초가 넘지 않도록 통제하는 것이다.

G1은 기존에 세대별 GC 를 넘어서 힙 메모리의 어느곳이든 회수 대상에 포함할 수 있도록 설계 되었다. 즉, '어느 영역에 쓰레기가 가장 많으냐' 와 회수했을 때 이득이 어디가 가장 크냐가 회수 영역을 고르는 기준이 된것이다. 이것이 G1의 혼합 GC (Mixed GC) 모드다.

기본 베이스는 여전히 세대 단위 컬렉션 이론이지만 힙 메모리 레이아웃이 매우 다르다. 하지만 기존에 크기와 수가 고정된 세대 단위 영역 구분이 아니라, 연속된 자바 힙을 '동일 크기의' 여러 독립 리전으로 나눈다. 각 리전은 필요에 따라 신세대, 에덴, 생존자, 구세대가 될 수 있다. 또한, '큰' 객체를 저장하기 위해 거대 리전 (humongous region) 이라는 특별한 유형도 활용한다.

G1은 어떻게 정지 시간 예측 모델이 가능한걸까? 그것은 리전을 고정된 신세대/구세대 방식으로 사용하는 것이 아니라 매번 적절한 수의 리전을 계획적으로 회수하는 방식을 선택했기 때문에 힙 전체를 회수해야 하는 상황을 피한다.

처리 방식을 조금 더 구체적으로 설명하자면, G1은 각 리전의 쓰레기 누적값을 추적한다 (값이란 회수에 드는 시간, 크기 등을 의미) 그리고 우선 순위 목록을 관리하며 사용자가 -XXX:MaxGCPauseMillis 옵션으로 설정한 정지 시간이 허용하는 한도 내에서 회수 효과가 가장 큰 리전부터 회수한다. 말그대로 '가비지 우선' 이다.

G1GC 는 CMS 와 다르게 마크 스윕 뿐만 아니라 컴팩트 과정도 함께 하게 된다. 그리고 G1의 동작은 다음 4단계로 나뉜다.

  1. 최초 표시
    -> GC 루트가 직접 참조하는 객체들을 표시한다, 즉 시작 단계 스냅숏을 생성한다. 굉장히 빨리 끝나는 작업이지만 사용자 스레드를 일시 정지해야한다.
  2. 동시 표시
    -> 최초 표시한 객체로부터 연결 된 전체 힙의 객체 그래프를 찾아서 회수할 객체를 찾는다. 시간이 걸리지만 사용자 스레드와 동시 실행 수행된다. 스캔이 끝내고 시작 단계 스냅숏과 비교한다.
  3. 재표시
    -> 사용자 스레드를 잠시 멈춰야한다. 시작 단계 스냅숏 이후 변경된 소수의 객체만 처리하면 됨으로 빨리 처리된다.
  4. 복사 및 청소
    -> 통계 데이터를 기초로 리전들을 회수 가치와 비용에 따라 목표한 일시 정지 시간에 부합하도록 회수 계획을 세운다. 회수할 리전을 선별하고, 살아남은 객체들은 빈 리전에 이주시킨다.

셰넌도어 GC

셰넌도어 GC 는 STW 의 한계를 명확하게 가지고 있는 G1GC 에서 저지연 GC 를 구현하기 위해서 만들어졌다. 동시 모으기 지원에서 G1은 여러 스레드를 이용해 병렬로 수행하지만 (STW) 셰넌도어는 이 과정을 사용자 스레드와 동시에 수행한다. G1GC 는 여전히 예측 가능한 지연 시간을 통해서 적은 리전을 회수하고 낮은 STW 시간을 가지고 있지만 완벽한 동시 Compaction 은 불가능하다. 셰넌도어 GC 의 경우 애플리케이션이 실행되는 동안에도 메모리를 이동할 수 있도록 설계되어있다.

profile
성장하는 사람

0개의 댓글