ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JVM] 가비지 컬렉터와 메모리 할당 전략 (2) - 클래식 가비지 컬렉터 > G1 GC
    DevBook/JVM 밑바닥까지 파헤치기 2025. 3. 18. 00:25

    G1 컬렉터(가비지 우선 컬렉터)

    • JDK 9부터 default gc

     

    힙 메모리 레이아웃

    • 부분 회수(partial collection)와 리전(region)을 회수 단위로 하는 메모리 레이아웃 적용
    • 힙 메모리의 어느 곳이든 회수 대상에 포함할 수 있음
      • 이를 회수 집합(collection set)이라 하며 CSet이라 함
      • 어느 세대에 속하느냐가 아니라 '어느 영역에 쓰레기가 가장 많으냐'와 '회수했을 때 이득이 어디가 가장 크냐'가 회수 영역을 고르는 기준이 됨 (G1의 Mixed GC 모드)
    • 영역 기반 힙 메모리 레이아웃 정지 시간 예측 모델 구현을 가능하게 함
      • G1도 여전히 세대 단위 컬렉션 이론에 기초하고 있지만, 힙 메모리 레이아웃은 다른 컬렉터와 다름
      • 크기와 수가 고정된 세대 단위 영역 구분이 아닌, 연속된 자바 힙을 동일 크기의 여러 독립된 리전으로 나눔
      • 각 리전은 필요에 따라 신세대의 에덴이나 생존자 공간이 될 수도, 구세대용 공간으로 쓰일 수도 있음
        • *g1 gc는 단일 survivor 영역 사용(다른 컬렉터처럼 survivor0, survivor1로 나누지 않음)
      • 리전을 회수 단위로 사용하기 때문에, 매번 적절한 수의 리전을 계획적으로 회수하여 자바 힙 전체를 회수해야 하는 상황을 피할 수 있음
    • '큰' 객체를 저장하기 위해 거대 리전(humongous region)이라는 특별한 유형도 활용함
      • 리전 용량의 절반보다 큰 객체를 '큰 객체'로 취급함
        • 리전 하나의 크기는 -XX:G1HeapRegionSize 매개변수로 설정
        • 1MB ~ 32MB까지 2의 제곱수
      • 리전 하나의 크기를 넘어서는 큰 객체는 N개의 연속적인 리전에 저장
      • 해당 리전들은 구세대로 취급
    • 신세대/구세대 개념을 사용하지만 세대가 고정되어 있지 않음
      • 리전별 역할을 동적으로 바꿀 수도 있고, 같은 역할의 리전이 연이어 배치될 필요도 없음

     

    처리 방식

    • G1은 각 리전의 쓰레기 누적값을 추적함
      • GC로 회수할 수 있는 공간의 크기와 회수에 드는 시간의 경험값 기반
    • 우선순위 목록을 관리하며 사용자가 -XX:MaxGCPauseMillis 매개변수로 설정한 일시 정지 시간(기본값 200ms)이 허용하는 한도 내에서 회수 효과가 가장 큰 리전부터 회수함 ('가비지 우선'이라는 이름이 탄생한 이유)
    • 메모리 공간을 리전 단위로 분할해 우선순위대로 회수함으로써 제한된 시간 내에 효율적으로 회수할 수 있게 됨

     

    G1이 해결해야 했던 주된 문제

    1. 객체들의 리전 간 참조 문제를 해결해야 한다.
      • 기억 집합을 도입하여 GC 루트부터 힙 전체를 스캔하는 일을 피해야 함
      • 모든 리전이 각자의 기억 집합을 관리하며, 기억 집합에는 다른 리전으로부터의 모든 참조 정보를 기록하고, 이 참조가 어떤 카드 페이지에 속하는지 표기함
      • G1의 기억 집합은 기본적으로 해시 테이블 구조
        • 키: 다른 리전으로부터의 시작 주소, 값: 하나의 집합(원소들은 카드 테이블의 인덱스 번호)
        • '내가 가리키는 대상'과 '나를 가리키는 대상'을 모두 기록하는 양방향 카드 테이블 구조
      • 이런 이유로 전통적인 컬렉터들보다 메모리를 많이 사용함 (최소 10~20% 정도, 계속해서 메모리 사용량 줄이는 방향으로 발전되고 있음)
    2. 동시 표시 단계 동안 GC 스레드와 사용자 스레드가 서로 간섭하지 않도록 보장해야 한다. 
      • GC 스레드와 사용자 스레드가 동시에 수행되어 발생하는 문제
        • GC 스레드가 객체 간 참조를 표시하는 동안 사용자 스레드가 객체 참조 관계를 수정할 수 있음
          • 원래의 객체 그래프 구조를 기반으로 표시를 하도록 해야 함
          • 이를 보장하지 않으면 표시 단계가 끝나고 오류가 발생할 수 있음
          • G1은 시작 단계 스냅샷 방식을 사용하여 보장
          • 참고) 동시 접근 가능성 분석
        • GC가 사용자 스레드가 만드는 새로운 객체를 위한 메모리 할당에 영향을 줌
          • 사용자 스레드가 계속 수행된다면 새로운 객체도 계속 만들어질 수 있음
          • G1은 각 리전을 위해 TAMS(top-at-mark-start)라는 두 개의 포인터를 설계
            • 리전의 공간 일부가 동시 회수 프로세스 동안 새로운 객체를 할당하기 위한 공간으로 나뉨
            • 동시 회수 동안 새로 생성되는 객체의 주소는 반드시 이 두 포인터보다 높은 주소 영역에 할당되어야 함
          • G1은 기본적으로 TAMS 포인터가 가리키는 주소보다 높이 있는 객체는 암묵적으로 표시된 것으로 간주함 (즉, 회수 대상에서 제외함)
          • 메모리 회수 속도가 메모리 할당 속도를 따라가지 못한다면 G1 역시 사용자 스레드들을 모두 멈추고 전체 GC를 수행해야 함 (stop-the-world 발생)
    3. 신뢰할 수 있는 정지 시간 예측 모델을 구현해야 한다.
      • G1의 정지 시간 예측 모델의 이론적 기초는 감소 평균(decaying average)
      • 감소 평균은 '최근'의 평균적인 상태를 더 정확하게 알려줌
        • GC가 이루어지는 동안 G1은 리전별 회수 시간, 리전별 기억 집합에서 더렵혀진 카드 개수 등 측정할 수 있는 각 단계의 소요 시간을 기록함. 이 정보로부터 평균, 표준 편차, 신뢰도 같은 통계를 분석함
        • 리전의 통계적 상태가 더 최근일수록 회수해서 얻는 가치를 더 높게 쳐줌
        • 다음 GC가 시작되면 이 정보를 기초로 어느 리전들을 회수해야 사용자가 기대하는 정지 시간 내에 가장 큰 효과를 거둘지 예측하는 것

     

    G1의 동작 단계

    1. 최초 표시
      • GC 루트가 직접 참조하는 객체들을 표시(depth 1까지만 표시)하고 TAMS 포인터의 값을 수정함
        • 시작 단계 스냅샷 생성
        • 사용자 스레드와 동시에 수행되는 다음 단계에서 새로운 객체들이 가용 리전에 올바르게 할당되도록 하기 위한 조치
      • 이 단계에서는 사용자 스레드를 일시 정지해야 함
        • 소요 시간이 매우 짧고 마이너 gc가 실행되는 시간을 틈타 동시에 끝남
    2. 동시 표시
      • GC 루트로부터 시작하여 객체들의 도달 가능성을 분석하고, 전체 힙의 객체 그래프를 재귀적으로 스캔하며 회수할 객체를 찾음
      • 이 단계는 사용자 스레드와 동시에 수행됨
        • 객체 그래프 스캔이 끝난 후 시작 단계 스냅샷과 비교하여 동시 실행 도중 참조가 변경된 객체들을 다시 스캔해야 함
    3. 재표시
      • 이 단계는 사용자 스레드를 일시 정지함
      • 시작 단계 스냅샷 이후 변경된 소수의 객체만 처리하면 되므로 매우 빠르게 끝남
    4. 복사 및 청소
      • 통계 데이터를 기초로 리전들을 회수 가치와 비용에 따라 줄 세운 후, 목표한 일시 정지 시간에 부합하도록 회수 계획을 세움
      • 회수할 리전들을 선별하고 선별된 리전들에서 살아남은 객체들을 빈 리전에 이주 시킴(복사 후 기존 리전은 말끔히 비움)
      • 생존한 객체를 이동시켜야 하므로 사용자 스레드를 일시 정지함
      • 다수의 GC 스레드가 이 작업을 병렬로 처리함

     

    참고) 정지 시간의 기대값 설정 시 주의사항

    더보기
    • 정지 시간의 '기대값'을 사용자가 설정할 수 있는 것은 G1의 매우 큰 장점
      • -XX:MaxGCPauseMillis (default 200)
      • 해당 값을 조율해 이상적인 '처리량 대 지연 시간' 균형점을 찾을 수 있음
    • 반드시 현실적인 기대값이어야 함
      • 목표 시간이 너무 짧으면 회수 속도가 새로 할당되는 속도를 따라잡지 못해 full gc가 일어나 성능 떨어뜨리게 됨
      • 적정한 기대 정지 시간은 100~200ms or 200~300ms 정도
    • G1을 시작으로 최신 가비지 컬렉터 대다수는 자바 힙 전체를 한 번에 청소하는 대신, 어플리케이션의 메모리 할당 속도(할당률)에 맞춰 회수하는 방향으로 변화함

     

    참고) G1 가비지 컬렉터의 GC 사이클

    더보기
    https://docs.oracle.com/javase/9/gctuning/garbage-first-garbage-collector.htm#JSGCT-GUID-ED3AB6D3-FD9B-4447-9EDF-983ED2F7A573

     

    Young-only (Minor GC)

    • Young GC는 Eden 영역이 가득 찰 때마다 수행됨
      • Eden 영역이 가득차면 Minor GC 발생
      • 살아남은 객체는 Survivor or Old 영역으로 이동 (age 값 기준)
      • 사용자 스레드는 일시 정지됨
    • Minor GC가 여러 번 반복 수행되다가 Old 영역이 일정 임계치를 초과하면 Mixed GC를 위한 준비 단계들이 수행됨
      • Initial Mark(최초 표시)
      • Concurrent Marking(동시 표시)
      • Remark(재표시)
      • Cleanup: Mixed GC 대상 리전들을 정함

    Space Reclamation

    • young 영역 + 일부 old 영역 대상으로 Mixed GC 수행

     

     

    https://docs.oracle.com/en/java/javase/21/gctuning/garbage-first-g1-garbage-collector1.html

    https://www.oracle.com/technical-resources/articles/java/g1gc.html

    댓글

Designed by Tistory.