GC 는 처리량, 메모리 사용량, 지연 시간 세가지 지표가 있다. 처리량과 메모리 사용량은 하드웨어가 발전하며 좋은 지표를 내기 쉽지만, 지연 시간은 그렇지 않다. 더 많은 용량의 RAM 이 개발될 수록 GC 를 위한 시간은 점점 더 멀어진다.
따라서 GC 는 지연 시간을 줄이는 방향으로 발전했다.
G1 GC 은 Young GC 및 Mixed GC 에서 생존한 객체를 다른 Region 으로 옮기는 소위 Compact 라 불리는 작업은 STW 로 동작한다.
저지연 컬렉터는 Compact 를 유저 스레드와 동시에 진행하여 아주 낮은 일시 정지로 낮은 지연 시간을 달성하는 컬렉터이다.
처리량 영향을 최소로 하면서 10ms 이하의 일시 정지 시간이 목표다.
섀넌도어는 레드햇에서 만든 저지연 컬렉터이다. 포워딩 포인터 기법을 이용하여 Compact 를 동시에 처리한다.
Oracle JDK 에서는 제공하지 않으므로, Oracle JDK 를 유료로 지원받을 계획이 있다면 사용하지 않는게 좋다.
세대구분 셰넌도어도 존재하나, 처음에는 세대구분이 없었다. 세대 구분 없는 셰넌도어 기준으로 설명한다.

https://wiki.openjdk.org/spaces/shenandoah/pages/25002018/Main?utm_source=chatgpt.com
OpenJDK 문서에 따르면 셰넌도어는 9가지 단계로 이뤄진다.
메모리 파편화를 피하기 위해서는 Compact 과정이 불가피하다. 즉, 객체가 다른 위치로 이동해야 한다는 것이다.
이 때 고려해야할 것은 두가지다.
첫번째로, 객체를 이동할 때 복사를 하고 기존 객체를 나중에 정리할 것이냐 or 새 위치에 새로운 객체를 생성하면서 기존 객체를 삭제 할 것이냐다.
언뜻 보면 객체를 두벌 관리하여 공간을 차지하는 복사보단 삭제후 생성이 좋아 보인다. 하지만 삭제 - 생성하게 되면 기존 객체가 사라지니 반드시 기존 객체에 대한 참조를 새로운 객체 참조로 변경하는 것도 같이 발생해야 한다. 하지만 특정한 하나의 객체를 새로 생성할 때 해당 객체를 사용하는 곳을 모두 찾아 참조를 변경하는 것은 STW 에서 일어나야 하며, 이는 시간이 많이 걸린다. 따라서 두벌을 유지하는 복사 이주 방식을 택한다.
두번째로, 그럼 복사 이동을 택했을 때 객체를 참조하는 곳의 참조값을 이전 참조값에서 새 참조값으로 변경하는 방법이다. 이는 아래에서 자세히 설명하겠다.

https://shipilev.net/talks/javazone-Sep2018-shenandoah.pdf
위 그림은 7번 동시 참조 갱신 중의 살아남은 객체의 상태다.
객체는 동시 이주 단계를 거쳐 두벌로 복사된 상태이다.
검정 네모는 GC Root 이며, 객체를 참조하는 GC Root 는 총 4개가 있다.
동시 참조 갱신 단계 중 2개의 GC Root 참조는 이주/복사된 새 객체를 참조하도록 갱신되었으나, 2개의 GC Root 참조는 아직 이주/복사 이전 객체를 참조한다. 이러한 타이밍은 반드시 있다.
이 때 만약 예를들어 참조를 옮기지 못한 왼쪽의 GC Root 를 통해 값의 수정이 이뤄지고(y=2 -> 4), 참조 갱신이 끝난 후 값을 조회한다면 수정되지 않은(y=2) 의 값을 읽어버릴 수 있다.
이를 위해 컬러 포인터 개념이 있다.

포워딩 포인터는 위와 같이 기존 객체 layout 앞에 추가된 포인터이다.
객체 레퍼런스를 가지고 있으며, 시스템에 따라 32/64 bits 이다.
이 포인터는 처음에는 위 그림처럼 자기 자신 객체 참조 주소를 가진다.

복사시점의 상태다. from-space(기존 객체) 와 to-space(복사/이동 된 객체) 의 포워딩 포인터는 모두 자기 자신을 가리킨다.

복사가 완료되면 from-space 포워딩 포인터를 to-space 를 가리키도록 변경한다.
이 동작에는 동시성 문제가 있는데,
위와 같이 복사 후 포워딩 포인터를 옮기기 전 mutator 스레드의 객체 참조가 발생하면 from-space 객체를 볼 수 있다.
따라서 쓰는 동작에서 CAS 를 이용한 write barrier 가 존재하여 to-space 객체에 쓸 수 있도록 동작한다.

이 그림은 참조 갱신 중에도 정상적으로 포워딩 포인터를 통해 to-space 의 객체를 수정하는 예시다.

모든 참조 갱신이 완료되면 기존 from-space 를 정리할 수 있다.
셰넌도어 1.0 에서는 두벌로 존재하는 객체에서 힙 일관성을 지키기 위해 모든 참조에 대해 Write/Read 배리어를 건다.
위의 코드에서 value 객체가 primitive type 이라도 barrier 를 건다.
barrier 를 쓰는 이유는 참조 타입 객체를 읽거나 쓸 때 해당 참조 타입 객체가 이동되는걸 방지하기 위한 것이라서, primitive type 은 사실 barrier 를 걸지 않아도 된다.
LoadReferenceBarrier 의 탄생으로 객체를 읽을때 to-space 에서 읽는것을 보장함으로서 barrier 의 수를 많이 줄일 수 있었다.
포워딩 포인터는 태초에 위처럼 마크 워드 앞에 자리를 차지했다.
마크 워드는 최하위 2비트의 플래그 비트로 여러 데이터를 표현할 수 있었는데, 그 자리에 뒀으면 안됐을까?
기존의 섀넌도어에서는 모든 객체 접근에서 to-space 를 보기 위해 마크워드 디코딩 “매번” 실행하여 검사하는 hotpath가 되어버려서 비용이 너무 컸다.
Load Reference Barrier 의 등장으로 포워딩 포인터는 mark-word 에 병합되었다.
이로 인해 메모리 사용량이 많이 줄게 되었다.
ZGC 는 오라클에서 주도로 만든 저지연 컬렉터이다. 마찬가지로 Compact 를 유저 스레드와 동시에 진행하여 저지연을 달성한다.
Oracle 문서를 보면 다음과 같이 표현되어 있다.
Scalable Low-Latency Garbage Collector
Scalable 이란, 힙의 크기나 객체의 양이 많아져도 여전히 낮은 일시 정지 시간과 높은 처리량을 낼 수 있다는 말이다.
세대구분 ZGC도 존재하나, 처음에는 세대구분이 없었다. 세대 구분 없는 ZGC 기준으로 설명한다.
ZGC 는 메모리를 Page 로 관리한다. (G1 의 리전과 비슷)
G1 과 다르게 ZGC 의 Page 는 소/중/대 세가지가 존재한다.
Page 는 동적으로 생성/파괴되며, 동적으로 바뀔 수 있다.

이 단계에서는 크게 세가지 일을 한다.
위는 마킹 루프 스켈레톤 코드다. 마크 스택의 객체를 pop 하여 mark_obj(bitmap 에 마킹)하고, MB(Marking Barrier) 로직을 실행한다.
아래는 마킹 배리어(MB) 는 마킹 단계에서 컬러포인터를 확인하는 과정, 로드 베리어(LB) mutate thread 의 객체 참조 과정에서 컬러포인터를 확인하는 과정을 보여준다.
이 단계에서는 객체 마킹과 이전 사이클 Relocation 객체의 Remapping 을 동시에 진행한다.
위 marking loop 스켈레톤 코드가 이 단계에서 진행하는 내용을 간략화 한 것이다. mark_obj() 함수에서는 bitmap 에 살아있음을 마킹하고, 이후에 해당 객체의 필드들을 순회하며 Mark Barrier 를 적용한다.
MB 에서는 컬러포인터를 확인하고 EC 에 포함되는 객체라면 Remapping, 아니면 컬러포인터 수정을 한 뒤 Marking Stack 에 쌓는다.
이 단계에서 사용되는 EC 는 이전 Cycle 에서 만들어진 EC
이 동작의 반복으로 살아있는 객체 그래프를 순회하며 Marking 과 Remapping 을 동시에 처리하게 된다. 그리고 그 첫 시작은 STW1 에서 Stack 에 넣은 GC Root 일 것이다.
따라서, 이 단계가 진행된 후에는 Dangling(Relocation 이전 주소를 가진 참조) 가 없음이 보장된다.
Mutator Thread 의 객체 참조시 사용되는 LB 와 다른점은 두가지다.
2번 때문에 같은 객체가 중복으로 Marking Stack 에 들어가는 부작용이 있지만, Bitmap Marking 동작은 CAS 로 Thread-Safe 하므로 문제는 없으며, Mutator Thread 의 Stack 에는 할일이 많지만 GC Thread 가 할일이 없어 Marking/Remapping Phase 가 끝남을 방지하기 위함이다. (G1 과 같이 이것도 Mutator Thread 에 로컬 스택이 있고, 글로벌 스택이 있는듯)
또한, ZGC 는 incremet-update 방식(Not SATB) 을 사용한다. Mutator Thread 에 의해 접근된 객체는 LB 로 Marking Stack 에 적재되며 동작한다.
마킹에 Mark0, Mark1 비트 두개가 사용되는 이유는 전 사이클의 Remapping 과 현 사이클의 Marking 이 동시에 진행되기 때문이다.
예를들어 전 사이클에서 마킹되었지만 Relocation 은 되지 않은 객체가 있다고 가정하자. 현 Marking/Remapping 에서 Çolor Pointer 는 0001. 현 사이클에서도 살아있는 객체라 새로 0010 으로 마킹 될 예정이다. 하지만 마킹이 진행되기 전에 Mutator Thread 로 부터 참조 조회가 있었고, LB 가 진행되는 상황이라 치자. 이 때 만약 Marking 을 나타내는 필드가 하나라면,이전 사이클에는 마킹했으나 현 사이클에는 아직 마킹을 하지 않아서 이번 사이클의 good color 로 색칠해야함을 표현할 수 없다. 비트가 하나라면 그냥 1 일 것이고 이번 사이클에서 마킹한 것인지 전 사이클에서 마킹한 것인지 구분할 수 없다는 것이다.
이는 결국 Marking 과 Remapping 을 동시에 진행함으로서 발생된 것이며, Marking 을 표현하는데 두 비트를 사용하는 것은 결국 성능상의 트레이드 오프라 할 수 있다.
Mark Stack 이 비어 모든 객체가 마킹 되길 기다리는 STW
이 단계는 Soft, Weak, Phantom Reference 를 처리하는 단계인데, 복잡하여 일단 생략
Evacuation Candidate (또는 Relocation Set) 은 살아남은 객체가 많지 않아 살아남은 객체를 다른 곳으로 이주시키고 해당 Page 를 회수할 Page 의 집합이다.
이 단계에서는 몇가지 정보를 삭제한다.
그리고 EC 를 만든다. 먼저 이번 GC Cycle 에서 새로 생성된 객체를 넣기 위해 STW1 에서 생성한 Page 를 제외한 모든 Page 중, 살아있는 객체가 적은 순으로 정렬(살아있는 객체 집계값은 marking 에서 생성)한 후, 파편화 임계값에 따라 Relocation 대상 Page 를 선정하여 EC 를 완성한다.
큰 Page 는 Relocation 대상이 아니다. EC 에는 소, 중 Page 만 포함
Mark0/1 과 Remapping 의 Load Barrier 차이
4번줄에 EC 로 변수는 같으나 Mark 단계 LB 의 EC 는 이전 Cycle EC 이고, Remapped 단계 EC 는 Selection of Evacuation Candidate 에서 선정한 EC 인 점이 다르다.
또한, EC 에 포함되었을때 왼쪽은 단순 remapping 이지만 오른쪽은 relocation 을 한다.

이 단계의 스켈레톤 코드는 위와 같다.
EC Page 의 객체들을 Relocation 하면서 Fowarding 테이블에 저장한다. Fowrading 테이블을 이후 LB, MB 에서 사용된다.
Relocation 이 끝난 Page 는 메모리에서 해제한다.
이 단계가 끝나서 객체가 모두 이주하더라도 여전히 이전 객체 참조값을 가진 곳이 있을 수 있다. 컬러포인터 + Barrier + Fowarding Table 덕에 이는 급하게 처리할 일은 아니며, 다음 사이클의 Marking/Remapping 단계에서 빠짐없이 처리될 것이다.
ZGC 는 동시 이주를 실현하기 위한 기술

컬러포인터는 객체 참조 주소값 자체에 포함되는 4비트의 메타데이터로서, GC 와 동시에 실행되는 유저의 객체 참조시 참조가 이동했더라도 정상적으로 이동한 참조를 찾아갈 수 있게 하는 메커니즘이다.
각 객체의 컬러포인터는 GC 사이클에서 Good 또는 Bad 상태를 나타낸다. Good 상태는 해당 참조값을 바로 사용할 수 있음이며, Bad 는 객체가 이동했을 수 있으니 체크해야한다(potentially invalid) 는 뜻이다.
참조값의 Load 시 미리 정의된 Load Barrier 트랩이 발동하여 Good 이라면 참조값을 반환하고, Bad 라면

| goodMask | badMask | |
|---|---|---|
| marked0 | 001 | 110 |
| marked1 | 010 | 101 |
| remapped | 100 | 011 |
GC Cycle 에서는 위 그림과 같이 현재 시점의 Good Color 가 정해진다. Mutator Thread 의 참조 또는 GC Thread 의 Marking 이 실행되면, 참조값의 Color pointer 와 현재 시점의 Good Color 를 비교하여 다르다면, Bad 상태로 간주하고 처리를 한다. 이 동작은 Barrier(LB/MB) 로서 동작한다.
Color Pointer 를 확인하고 참조를 최신화하는 이 동작은 3군데서 발생한다.
각 단계에서 실제로 어떤 단계로 어떻게 처리하는지는 위의 Cycle 설명에서의 스켈레톤 코드를 참조하자.
위 Cycle 에서 설명했듯, ZGC 는 Relocation 직후에 이동한 참조값에 대해 STW 를 하여 갱신하지 않는다.
유저 스레드 동작과 함께 천천히 진행한다.
이는 객체 조회시 Relocation 이전 참조를 가지고 있다 하더라도, Color Pointer 를 통해 이전 참조임을 판단하고 Fowarding Table 을 통해 새 참조값을 가져오는 동작을 객체를 참조하는 Mutator Thread 가 직접 할 수 있기 때문이다.
이를 Self-Healing 이라 한다.

https://dev.java/learn/jvm/tool/garbage-collection/zgc-deepdive/
위 그림은 아마 객체 주소에 42bits 를 사용하던 시절의 그림이라 4, 8, 10 이 된것 같다. 44bits 를 사용한다면 1, 2, 4 가 되어야 할 듯.
Color Pointer 는 객체 참조 주소값 자체에 저장된다. 주소를 나타내는 값을 임의로 변경했다. ZGC 가 관리하는 메모리 공간에서는 이를 알고 있으나, OS 는 다르다. Color Pointer 가 다르면 다른 주소로 인식 할 뿐이다.
하지만 컬러포인터가 다르더라도 이들은 모두 같은 객체다. 따라서 자바의 가상 주소 공간에서 Color Pointer 로 다른 주소 공간을 나타내더라도 OS 메모리 공간에는 하나의 메모리 주소를 표현하도록 Multi-Mapping 을 사용한다.
한 리전안의 객체들이 이동하면 그 즉시 해당 리전을 재사용할 수 있다. Self Healing 덕분이다. 섀넌도어처럼 이전 객체를 유지 할 필요가 없다.
Read Barrier(LB/MB) 만 가지고도 처리할 수 있으므로 장벽의 수가 많이 줄어든다.
아직 자바 객체 주소에는 사용하지 않는 Bit 가 많이 남아서, 확장 가능성이 열려 있다.
세대 구분 이론은 세가지 가설로 탄생했다. 그 중 대다수 객체는 일찍 죽는다는 가설로 Young GC 를 자주 실행시킨다. 이는 곧 죽을 확률이 높은 객체만을 GC 판별한다는 것이다. 이는 GC 의 CPU 사용 효율을 높일 수 있다.
ZGC 나 섀넌도어는 처음에는 세대 구분 없이 이를 출시했다. 세번째 가설에 의한 세대간 참조를 해결하는 기억 집합이 필요 없었기에, 메모리 사용량은 약간 줄 수 있었다.
하지만 결국 매 GC 마다 (세대 구분 GC 였다면 Old 였을) 오래 살아남는 객체를 매번 판별해야했다. 이것이 결국 ZGC, 섀넌도어도 세대 구분으로 발전하게된 이유다.
세대 구분 ZGC 는 높은 CPU 효율로 단일 세대 ZGC 보다 높은 처리량을 달성할 수 있다.
또한, 일시 정지 시간이 줄어들기에 P99 Latency 를 완화할 수 있다.
단일 세대 GC 의 가장 큰 문제는 할당 지연(allocation stall) 이다. 이는 GC 속도가 객체 할당속도보다 느릴 때 발생한다. 세대 구분 ZGC 는 같은 일시 정지 시간에 더 많은 객체를 정리 할 수 있으므로 이 또한 많이 완화되었다.
https://inside.java/2023/11/28/gen-zgc-explainer/
저지연 가비지 컬렉터는 회수조차 유저 스레드와 동시에 실행하기 때문에 극단적인 일시정지 시간 감소를 가져올 수 있었다.
일시 정지 시간이 길면 어떤게 나쁠까? P99 의 유저 경험이 나빠질 수 있다는 것이다.
한번씩 발생하는 긴 일시정지는 유저에게 한번씩 큰 지연 시간을 겪게 할 것이다.
반면 유저 스레드와 동시에 실행하는 것들은 많아지기 때문에 전체적인 유저의 지연 시간은 좀 더 늘어날 수 있다. CPU 자원을 나눠 쓰며 컨텍스트 스위칭 등이 일어날 수 있기 때문이다.
정리하자면 이렇다.
긴 정지 시간을 줄일 수록 P99 의 Latency 는 완화할 수 있으나 전체적인 Latency 는 조금 늘어날 수 있으나.
긴 정지 시간을 허용하면 그 시간동안 오롯이 GC 를 실행하여 전체적인 처리량 및 Latency 는 줄어든다. 하지만 긴 정지 시간에 요청을 보낸 유저는 긴 시간 후 응답을 받을 것이다.
따라서 유저 대상 서비스와 연결된 자바 애플리케이션이라면 정지 시간을 줄이는 저지연 컬렉터가 좋다.
반대로 유저를 대상으로 하지 않고 뒷단에서만 동작하는 배치, 데이터 정제 등의 작업 들은 처리량 컬렉터가 좋을 것이다.
이론상은 위와 같은데, 현재 많은 연구결과들은 처리량이든 지연시간이든 ZGC 가 좋다~ 라는 것 같다. 이는 참고만 하고 각자 환경에서 테스트하는게 제일 중요할 것.
ZGC 기대 가능한 장점
- P99 응답 지연의 감소
기대 가능한 단점
- barrier 의 증가
- threadlocal handshake
- gc 와 application 의 리소스 경쟁으로 인한 처리량 감소
- zgc 의 경우 compressed oops 의 사용 불가
https://netflixtechblog.com/bending-pause-times-to-your-will-with-generational-zgc-256629c9386b
[논문] Shenandoah - Christine H. Flood...
https://tech-notes.accel.dk/programming/java/java12/docs/PPPJ2016.pdf?utm_source=chatgpt.com
shipliev 이사람 자료들이 좀 자세하고 좋음..
https://shipilev.net/talks/javazone-Sep2018-shenandoah.pdf
openjdk
https://wiki.openjdk.org/spaces/shenandoah/pages/25002018/Main?utm_source=chatgpt.com
섀넌도어 제작한 redhat 자료.
https://developers.redhat.com/blog/2019/06/27/shenandoah-gc-in-jdk-13-part-1-load-reference-barriers?p=602377
https://developers.redhat.com/blog/2019/06/28/shenandoah-gc-in-jdk-13-part-2-eliminating-the-forward-pointer-word?utm_source=chatgpt.com
https://developers.redhat.com/blog/2019/07/01/shenandoah-gc-in-jdk-13-part-3-architectures-and-operating-systems?p=606497#
https://developers.redhat.com/articles/2021/05/20/shenandoah-garbage-collection-openjdk-16-concurrent-reference-processing?utm_source=chatgpt.com#traditional_reference_processing
https://developers.redhat.com/articles/2024/05/28/beginners-guide-shenandoah-garbage-collector?utm_source=chatgpt.com#
jep
404 - generational Shenandoah
[논문] Deep Dive into ZGC: A Modern Garbage Collector in OpenJDK - Albert Mingkun Yang..
기본 개념 설명 여기서 보는게 제일 좋음
https://dl.acm.org/doi/full/10.1145/3538532
Naver D2 개념 설명. 2등. 같이 보기.
https://d2.naver.com/helloworld/0128759#ch1-3
https://www.blog-dreamus.com/post/zgc%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C
https://dev.java/learn/jvm/tool/garbage-collection/zgc-deepdive/
openjdk
https://wiki.openjdk.org/spaces/zgc/pages/34668579/Main
ppt 그림자료 좋음
https://www.jfokus.se/jfokus18/preso/ZGC--Low-Latency-GC-for-OpenJDK.pdf
https://cr.openjdk.org/~pliden/slides/ZGC-PLMeetup-2019.pdf
jep
333 - A Scalable Low-Latency Garbage Colloector
439 - Generational ZGC
490 - Remove the Non-Generational Mode
세대구분 ZGC
https://inside.java/2023/11/28/gen-zgc-explainer/
ZGC Lower Tail Latencies(P99)
https://www.morling.dev/blog/lower-java-tail-latencies-with-zgc/
Netflix 세대구분 ZGC 전환
https://netflixtechblog.com/bending-pause-times-to-your-will-with-generational-zgc-256629c9386b
AWS EMR HBase 에서 G1 vs ZGC
https://aws.amazon.com/ko/blogs/big-data/improve-amazon-emr-hbase-availability-and-tail-latency-using-generational-zgc/
parallel vs g1 vs zgc : throughput, latency..
https://kstefanj.github.io/2021/11/24/gc-progress-8-17.html