-
[JVM] 가비지 컬렉터와 메모리 할당 전략 (2) - 클래식 가비지 컬렉터 > G1 GCDevBook/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이 해결해야 했던 주된 문제
- 객체들의 리전 간 참조 문제를 해결해야 한다.
- 기억 집합을 도입하여 GC 루트부터 힙 전체를 스캔하는 일을 피해야 함
- 모든 리전이 각자의 기억 집합을 관리하며, 기억 집합에는 다른 리전으로부터의 모든 참조 정보를 기록하고, 이 참조가 어떤 카드 페이지에 속하는지 표기함
- G1의 기억 집합은 기본적으로 해시 테이블 구조
- 키: 다른 리전으로부터의 시작 주소, 값: 하나의 집합(원소들은 카드 테이블의 인덱스 번호)
- '내가 가리키는 대상'과 '나를 가리키는 대상'을 모두 기록하는 양방향 카드 테이블 구조
- 이런 이유로 전통적인 컬렉터들보다 메모리를 많이 사용함 (최소 10~20% 정도, 계속해서 메모리 사용량 줄이는 방향으로 발전되고 있음)
- 동시 표시 단계 동안 GC 스레드와 사용자 스레드가 서로 간섭하지 않도록 보장해야 한다.
- GC 스레드와 사용자 스레드가 동시에 수행되어 발생하는 문제
- GC 스레드가 객체 간 참조를 표시하는 동안 사용자 스레드가 객체 참조 관계를 수정할 수 있음
- 원래의 객체 그래프 구조를 기반으로 표시를 하도록 해야 함
- 이를 보장하지 않으면 표시 단계가 끝나고 오류가 발생할 수 있음
- G1은 시작 단계 스냅샷 방식을 사용하여 보장함
- 참고) 동시 접근 가능성 분석
- GC가 사용자 스레드가 만드는 새로운 객체를 위한 메모리 할당에 영향을 줌
- 사용자 스레드가 계속 수행된다면 새로운 객체도 계속 만들어질 수 있음
- G1은 각 리전을 위해 TAMS(top-at-mark-start)라는 두 개의 포인터를 설계함
- 리전의 공간 일부가 동시 회수 프로세스 동안 새로운 객체를 할당하기 위한 공간으로 나뉨
- 동시 회수 동안 새로 생성되는 객체의 주소는 반드시 이 두 포인터보다 높은 주소 영역에 할당되어야 함
- G1은 기본적으로 TAMS 포인터가 가리키는 주소보다 높이 있는 객체는 암묵적으로 표시된 것으로 간주함 (즉, 회수 대상에서 제외함)
- 메모리 회수 속도가 메모리 할당 속도를 따라가지 못한다면 G1 역시 사용자 스레드들을 모두 멈추고 전체 GC를 수행해야 함 (stop-the-world 발생)
- GC 스레드가 객체 간 참조를 표시하는 동안 사용자 스레드가 객체 참조 관계를 수정할 수 있음
- GC 스레드와 사용자 스레드가 동시에 수행되어 발생하는 문제
- 신뢰할 수 있는 정지 시간 예측 모델을 구현해야 한다.
- G1의 정지 시간 예측 모델의 이론적 기초는 감소 평균(decaying average)
- 감소 평균은 '최근'의 평균적인 상태를 더 정확하게 알려줌
- GC가 이루어지는 동안 G1은 리전별 회수 시간, 리전별 기억 집합에서 더렵혀진 카드 개수 등 측정할 수 있는 각 단계의 소요 시간을 기록함. 이 정보로부터 평균, 표준 편차, 신뢰도 같은 통계를 분석함
- 리전의 통계적 상태가 더 최근일수록 회수해서 얻는 가치를 더 높게 쳐줌
- 다음 GC가 시작되면 이 정보를 기초로 어느 리전들을 회수해야 사용자가 기대하는 정지 시간 내에 가장 큰 효과를 거둘지 예측하는 것
G1의 동작 단계
- 최초 표시
- GC 루트가 직접 참조하는 객체들을 표시(depth 1까지만 표시)하고 TAMS 포인터의 값을 수정함
- 시작 단계 스냅샷 생성
- 사용자 스레드와 동시에 수행되는 다음 단계에서 새로운 객체들이 가용 리전에 올바르게 할당되도록 하기 위한 조치
- 이 단계에서는 사용자 스레드를 일시 정지해야 함
- 소요 시간이 매우 짧고 마이너 gc가 실행되는 시간을 틈타 동시에 끝남
- GC 루트가 직접 참조하는 객체들을 표시(depth 1까지만 표시)하고 TAMS 포인터의 값을 수정함
- 동시 표시
- GC 루트로부터 시작하여 객체들의 도달 가능성을 분석하고, 전체 힙의 객체 그래프를 재귀적으로 스캔하며 회수할 객체를 찾음
- 이 단계는 사용자 스레드와 동시에 수행됨
- 객체 그래프 스캔이 끝난 후 시작 단계 스냅샷과 비교하여 동시 실행 도중 참조가 변경된 객체들을 다시 스캔해야 함
- 재표시
- 이 단계는 사용자 스레드를 일시 정지함
- 시작 단계 스냅샷 이후 변경된 소수의 객체만 처리하면 되므로 매우 빠르게 끝남
- 복사 및 청소
- 통계 데이터를 기초로 리전들을 회수 가치와 비용에 따라 줄 세운 후, 목표한 일시 정지 시간에 부합하도록 회수 계획을 세움
- 회수할 리전들을 선별하고 선별된 리전들에서 살아남은 객체들을 빈 리전에 이주 시킴(복사 후 기존 리전은 말끔히 비움)
- 생존한 객체를 이동시켜야 하므로 사용자 스레드를 일시 정지함
- 다수의 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
'DevBook > JVM 밑바닥까지 파헤치기' 카테고리의 다른 글
[JVM] 가상 머신 실행 서브시스템 (1) - 클래스 파일 구조 (0) 2025.03.29 [JVM] 가비지 컬렉터와 메모리 할당 전략 - 메모리 할당과 회수 전략 (0) 2025.03.27 [JVM] 가비지 컬렉터와 메모리 할당 전략 (2) - 클래식 가비지 컬렉터 (0) 2025.03.18 [JVM] 가비지 컬렉터와 메모리 할당 전략 (1) - 기본 알고리즘 (0) 2025.03.17 [JVM] 자동 메모리 관리 - 자바 메모리 영역과 메모리 오버플로우 (1) 2025.03.15