대용량 메모리를 갖춘 멀티프로세서 시스템을 대상으로 합니다. G1은 최소한의 설정으로 높은 처리량을 달성하면서 가비지 컬렉션 일시 중지 시간 목표를 높은 확률로 충족하도록 설계되었습니다. G1은 다음과 같은 특징을 가진 현재의 대상 애플리케이션 및 환경을 사용하여 지연 시간과 처리량 간의 최적의 균형을 제공하는 것을 목표로 합니다.
https://docs.oracle.com/en/java/javase/11/gctuning/garbage-first-g1-garbage-collector1.html
mark-copy, 정리떄에 한 리전으로 모인다는 개념은 mark-compact-XX:+UseG1GC 로 명시적으로 활성화 할 수 있다.일시 정지 시간 조절 : Young GC 기록을 통해 Young Gen 의 크기를 수정함. GC 에 걸리는 시간이 조절됨.일시 정지 시간 조절 : Marking 은 Concurrent(SATB)일시 정지 시간 조절 : CMS 에 비해 Compact 가 있으므로 메모리 단편화 X. Full GC 의 빈도가 낮음.높은 처리량 : 살아남은 객체를 Copy, Compact 하는 과정(오래 걸리는 작업)은 STW 상태에서 병렬로 실행됨둘 다CMS 와 다르게 회수 단계에 Mark-Compact 를 사용한다. 객체를 이동시키는 과정에서는 유저 스레드와 동시에 실행하지 않으므로 이론상 CMS 보다는 정지 시간이 길 수 있다.
하지만 최대한 짧은 시간이 목표인 CMS 와 최대의 처리량과 목표한 정지 시간이라는 G1 의 정지 시간 차이는 납득할 만 하며, 실제로 거의 나지 않는다.

-XX:G1HeapRegionSize 로 설정
Young GC 와 Concurrent marking 을 진행하는Young-Only Phase 와 Mixed GC 진행하는 Space Reclamation Phase 의 반복

https://tschatzl.github.io/2022/08/04/concurrent-marking.html
SATB 를 만들고, Marking 을 시작하기 위해 Root 를 마킹하는 단계이다.
Concurrent Marking 의 시작이며, IHOP 임계점을 넘으면 시작된다.
동시 마킹은 Mixed GC 에서 Old Generation 에서 살아있는 객체를 Marking 하는 것이다.
Old 영역에서 살아남아야 할 객체는 다음 두가지로, 둘 다 Root 로서 마킹하고 객체 그래프를 순회해야한다.
- 스택 등 에서 참조중인 객체
- Young Gen 에서 참조중인 객체
1번은 별 문제가 없는데, 2번은 문제가 있다. Young GC 에서 세대간 참조인 Young Gen <- Old Gen 참조는 RSet 을 통해 쉽게 해결이 가능하지만, Old GC 에서 세대간 참조인 이 경우는 RSet 으로 해결이 불가능하다. 따라서 Young Gen 을 모두 스캔하여 Old Gen 으로의 참조가 있는지 체크해야 한다. 이를 위해
Concurrent Marking Start는 Young GC 에 얹혀 실행되며 Young GC 의 스캔에 껴서 Old Gen 으로의 참조가 있는 Young Region 을 골라둔다.
Young -> Old 로의 세대간 참조가 RSet 만으로 해결되지 않는다는건.. 이 글에서 참고했는데, 근거는 명확하진 않다.
내 생각으로는 oracle 문서 에서 Young Gen 의 RSet 은 항상 유지하지만, Old Gen 의 RSet 은 lazily 하게 완성되기 때문에 완벽하지 않기 때문이지 않나 싶다. Old Gen 의 RSet 을 lazily 하게 세팅되는 이유는 Old Gen 은 리전이 엄청 큰데 Rset 을 유지하면 메모리가 많이 들어서 그렇다. (이슈)
Young GC 와 Mixed GC 둘 다 크게 네가지 단계로 이뤄진다.
다른건 CSet 이 Young Gen Region 이냐, Young Gen Region + Old Gen Region 이냐다.
최대 일시 정지 시간 설정인 -XX:MaxGCPauseTimeMillis 와 GC 간격인 -XX:GCPauseIntervalMillis 는 일시 정지 시간에 영향을 미칠 수 있다.
이 옵션으로 셋팅된 정지 시간 목표를 달성하기 위해 긴 시간 관찰된 비슷한 크기의 young gen 이 GC 되는데 걸린 시간, GC 과정에서 복사했던 객체의 수, 등등의 정보를 근거로 Young GC 가 끝날 때 마다 Young 영역의 크기를 다시 결정한다.
-XX:G1NewSizePercent=5, -XX:G1MaxNewSizePercent=60
이 두 사이의 값에서 G1 가 동적으로 Young Gen 의 크기를 결정한다. 값은 전체 힙 크기 중 Young Gen 의 비율이다.
이렇게 Young GC 는 Young Gen 의 크기를 매번 조정해가면서 목표한 일시 정지 시간을 맞추는데 노력한다.
XX:G1MixedGCCountTarget = 8 : 몇번 실행할지 결정Mixed-XX:G1NewSizePercent 값으로 고정.-XX:G1MixedGCLiveThresholdPercent=85 옵션으로 필터링 되어 있는 상태. 즉, 15% 이상이 죽은 객체인 리전만 들어 있고, 그게 GC 대상 Region.XX:G1HeapWastePercent=5 : space-reclamation 를 종료하는 트리거. 전체 힙 크기 대비 CSet 에 남은 미회수 공간 크기의 합의 비율이 이 값보다 낮다면 space-reclamation 를 중지. 쉽게 말하면, CSet 에 정리할 공간이 적게 남았다면 끝낸다.
https://www.alibabacloud.com/blog/a-detailed-explanation-of-jvm-garbage-collector-g1%26zgc_601536
기존의 Serial GC 에서의 Card Table 은 Old Gen 의 크기를 일정 크기 Page 로 나눈 뒤 해당 영역에 대한 Dirty Check 만 하는 Byte Array 이다. 이는 Young Gen 하나 Old Gen 하나의 구조이기 때문에 가능했다. Young Gen 에서 Dirty 표시를 하면 참조하고 있는 대상인 비회수영역은 Old Gen 하나밖에 없다는 말이다.
하지만 Region 여러개로 나뉜 G1 GC 에서는, 회수 대상 Region 의 객체를 참조하는 비회수 대상 Region 이 여러개일 수 있다. 따라서 G1 에서는 리전간 참조를 기억하기 위해 Remember Set(RSet) 이라는 새로운 구조를 만들어 사용한다.
G1 은 기존과 같이 (기본적으로 힙을 512 byte 단위로 나눠서) Card Table 을 관리한다. 다만 Region 마다 존재한다. RSet 은 카드 테이블에 dirty 된 영역이 어떤 외부 Region 으로부터의 참조 때문인지를 인덱스로 관리하는 것이다.
G1 은 Young-Only Phase 에서 Young Gen 에 있는 객체만 정리한다. 이 때 Old Gen Region 에서 참조하고 있는 객체는 정리하면 안된다. 또한 살아남은 객체를 복사 및 이동시에 Old Gen 의 참조를 변경시켜줘야 한다. 이럴 때를 위해 기억집합이 존재한다.
Mutate Thread(유저 스레드) 는 객체의 포인터를 변경(살아남은 객체 이동 등)시에 Post Write Barrier 로 Card Table 에 Dirty 표시를 하고, 해당 사실을 Log Buffer 에 저장한다. Concurrent Refinement Threads 는 버퍼에서 Log 를 꺼내 RSet 을 업데이트한다.
이런 지연된 구조는 한꺼번에 RSet 을 처리하여 효율을 높이기 위해서다.
Concurrent Marking 단계에서 RSet 을 최신화 시키는 단계가 있다.
가비지 컬렉션의 Merge Heap Roots 단계에서는 중복을 제거하기 위해 리전당 RSet 으로 부터 Global RSet 를 만드는 단계가 있다.
RSet 은 리전마다 저장하므로 메모리 소비가 G1 내부에서 큰 편이다. 특히 Young Gen 의 크기를 작게 설정할 수록 그만큼 Old Gen 으로 승격되는 객체가 많아져 RSet 의 크기가 커질 수 있음을 유의하자.
CSet 은 정리 및 복사(Evacuate) 할 대상 Region 의 집합.
CSet 은 GC 의 Pre Evacuate Collection 단계에서 정해진다.
Young GC : Young Gen 의 전체가 CSet 이 된다.
Mixed GC : Young Gen 전체 + Concurrent Marking 의 결과로 Mixed GC 의 pre-evacuation. 에서 만든 것.
Young GC 는 전체 Young Gen 이 CSet 이기 때문에 고르고 뭐고 하는 과정은 없다.
Mixed GC 에서 사용할 CSet 은 다음과 같이 정해진다. [참고] [참고]
1. Concurrent Marking의 Remark 에서 점유율이 낮은 영역, 즉 여유 공간이 많은 영역을 선택하여 CSet Candidate 을 선정.
2. Concurrent Marking 의 Clean Up 에서 CSet Region 의 점유율과 연결성을 고려하여 GC 효율 순으로 정렬하여 최종 CSet Candidate 를 만듦. (Refine)
3. Mixed GC 에서는 이 정렬된 CSet Candidate 중 N 개의 CSet 을 선택하여 정리. Mixed GC 는 Space Reclaimation 단계에서 여러번 실행될 수 있으며, 각각의 GC 에서 CSet Candidate 중 CSet 를 선택.
Recaimation Phase 로 넘어가기 위해 Young-Only Phase 에서 Concurrent Marking 을 시작할 임계점.
-XX:InitiatingHeapOccupancyPercent=45 로 셋팅할 수 있다.
G1 은 Adaptive IHOP 기능을 지원한다. 처음 두세번의 Concurrent Marking 시작 임계점은 -XX:InitiatingHeapOccupancyPercent=45 으로 하되, 몇번의 GC Cycle 로 정보가 쌓인 후에는 처리량과 정지 시간에 알맞은 IHOP 를 자동으로 정해준다. -XX:+G1UseAdaptiveIHOP 로 켤 수 있다.
이렇게 G1 GC 가 자동으로 현재 상태에 맞게 자원이나 옵션을 자동으로 변경하는 것을 Ergonomic 이라 한다.
운영체제에 힙을 할당/반환하는 과정은 동시 마킹의 Remark 또는 Full GC 의 STW 상태에서만 진행한다.
하지만 G1 은 Full GC 를 최대한 피하는 방식으로 설계되었고, 애플리케이션이 유휴상태라 새로운 객체가 생성되지 않아 동시 마킹이 시작되는 임계값(IHOP) 을 넘지 못한다면 운영체제에 반환할 수 있는 Heap 메모리가 있다 하더라도 반환하지 못하게 될 수 있다. 이런 현상은 On-Demand Cloud 환경에서 비용적으로 불리할 수 있다.
-XX:G1PeriodicGCInterval
-XX:G1PeriodicGCSystemLoadThreshold
리전의 크기의 반 이상 크기인 객체를 거대 객체(Humongous Objects
)라 한다.
거대 객체는 Old Gen 의 연속된 리전인 거대 리전에 저장된다. 만약 1.5개 리전의 크기의 객체라면 2개의 리전을 차지하며 나머지 0.5 Region 은 쓰이지 않는다.
이러한 거대 객체는 Concurrent Marking 의 CleanUp 또는 Full GC 에서만 Reclaim 될 수 있다. 다만 기본타입 거대 객체의 경우 GC Pause 도중 회수할 수 있다.
거대 리전은 연속된 거대한 Region 을 필요로 하기 때문에 Full GC 를 발생시킬 확률이 있다.
또한 거대 객체는 Full GC 중에도 Compaction 을 위해 이동되지 않는다. 따라서 거대 객체가 많아지면 파편화가 심해져 메모리 부족 오류를 만날 수 있다.
더욱이, 메모리 공간이 충분하더라도 연속된 공간을 찾지 못해 객체 할당 실패 오류를 겪을 가능성도 있다. 이럴때는 Region Size 를 늘려 Region 이 수용할 수 있는 크기를 늘리거나, 힙 자체를 늘리는 방법 밖에 없다.
gc+heap=info 옵션에서 거대 객체의 개수를 알 수 있다.
SATB 는 marking 을 위한 객체 그래프 스캐닝 중 유저 스레드에 의해 참조 삭제가 일어날 경우 그 사실을 기록한 후 나중에 적용한다. 즉, 마킹 시작시에 살아있던 객체는 끝까지 살아있는 것으로 간주한다.
mutator thread 가 만약 A.a = b 인 상태에서 A.a = c 로 참조 변경을 했다고 치자. 이 때 A.a -> b 로의 참조는 사라지게 되는 것이다.
이 때 Pre Write Barrier 가 동작하여 기존 A.a 에 들어있던 b 참조를 SATB Buffer 에 저장한다.
이후 SATB Buffer 에 있던 참조(b) 는 marking 단계에서 GC Root 로 등록된다.
A.a -> b 의 참조가 사라진 것인데, SATB Buffer 에는 b 값만 저장해도 될까? 어디서 온 참조인지는 저장하지 않아도 될까?
SATB 의 목적은 '시작단계에 살아있던건 살아있다 간주' 이기 때문에, 삭제 대상이던 b 가 살아있도록만 만들어주면 된다. b 를 GC Root 로 등록하면 b 부터 시작하여 하위의 참조 객체들이 다 살아남을 수 있다. 그렇기 때문에 SATB Buffer 에는 b 만 넣으면 된다.
SATB 는 같은 문제를 해결하는 incremental-update 방식에 비해 일시 정지 시간을 줄여줄 수 있다. Marking 과 동시에 수정되는 객체 참조의 유형은 대부분이 새로운 객체의 생성이기 때문이다.
삭제된 객체만 관심을 가지는 SATB 방식이 새로운 참조에 관심을 가지는 incremental-update 보다 할일이 적다. [참고 : Compile-Time Concurrent Marking Write Barrier Removal]
SATB 의 단점도 있다.
Concurrent Marking 의 초기 단계에서 SATB 를 확정하면 이후 Marking 이나 Mixed GC 는 모두 시작단계 스냅샷 기준으로 동작한다. 즉, SATB 직후에 죽은 객체가 있더라도 다음 사이클이 도래하여 SATB 를 다시 찍기 전에는 죽은걸로 인식되지 않는다.
이런 객체를 부유 쓰레기(floating garbage. 실제로 죽었는데 살아있는것 처럼 보여 회수되지 못함) 라 하며, 메모리 사용량을 높게 한다.
Old Gen 의 힙 점유율이 높아 객체를 할당하지 못하면 Full GC 가 발생한다. 특히 거대 객체를 할당할 때 발생할 수 있다.
이러한 Full GC 는 긴 STW 를 발생시켜서, 최대한 줄이는 노력을 해야한다.
GC Log 에 Pause Full (Allocation Failure) 로 표시된다.
FUll GC 를 줄이기 위한 방법은 다음과 같다.
1. 거대 객체를 확인하고 리전 사이즈 늘리기
2. 힙 사이즈 늘리기
3. -XX:ConcGCThreads 로 동시 처리 스레드 늘리기
4. IHOP 수정으로 동시 마킹 더 빠르게 하기
G1 에는 Ergonomic 이라는 애플리케이션이 실행되며 자동으로 수정되는 값들이 있다. 이를 알아두면 좋을 것.

https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-g1-garbage-collector1`Ergonomic` 이라는 .html#GUID-082C967F-2DAC-4B59-8A81-0CEC6EEB9016
-XX:+UseG1GC -XX:G1HeapRegionSize=1m -XX:InitiatingHeapOccupancyPercent=20 -XX:MaxGCPauseMillis=50 -Xlog:gc*=debug
Mixed GC 가 일어날 수 있게 Concurrent Marking 이 발생하는 임계값을 좀 낮춰준다.
-Xlog:gc 에는 ergo, heap, age... 많은 태그를 같이 쓸 수 있는데, 모두 볼 수 있게 * 와 레벨을 debug 로 해줄 수 있다.
마킹 전체 내용 - https://tschatzl.github.io/2022/08/04/concurrent-marking.html
전체 내용 잘 설명 - https://www.alibabacloud.com/blog/a-detailed-explanation-of-jvm-garbage-collector-g1%26zgc_601536
oracle 문서, 버전마다 조금씩 다르다 - https://docs.oracle.com/en/java/javase/21/gctuning/garbage-first-g1-garbage-collector1.html#GUID-572C9203-AB27-46F1-9D33-42BA4F3C6BF3
redhat. GC 과정에 전체 리전이 어떻게 변화하는지 그림이 잘되어 있음 - https://www.redhat.com/en/blog/part-1-introduction-g1-garbage-collector
redhat. jdk8 기준으로 로그 보면서 설명. - https://www.redhat.com/en/blog/collecting-and-reading-g1-garbage-collector-logs-part-2
G1 논문. 기본적인 아이디어가 거의 다 설명되어 있음 - Garbage-First Garbage Collection. David Detlefs..
jdk20 bitmap 관련 변경. 이런 식으로 메모리 사용량이 줄고 있음. - https://github.com/openjdk/jdk/pull/8957
활동의 거의 없는 애플리케이션의 주기적 GC JEP - https://openjdk.org/jeps/346
RSet, Write Barrier 등 - https://www.jfokus.se/jfokus17/preso/Write-Barriers-in-Garbage-First-Garbage-Collector.pdf
그 외
1. https://www.oracle.com/technical-resources/articles/java/g1gc.html#Taming
2. https://tschatzl.github.io/2025/02/21/new-write-barriers.html
3. https://tschatzl.github.io/2021/02/26/early-prune.html