# Chapter 3. Garbage Collector & Memory Allocation Strategy (2/2)

## 오늘 날의 가비지 컬렉터들

<figure><img src="https://2804243633-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfV6clXG953LqFieNgWUf%2Fuploads%2Ft3YprRFOHrURIpVHd7k9%2Fimage.png?alt=media&#x26;token=3945c48a-cda5-45e3-b9d3-a06f16134978" alt=""><figcaption></figcaption></figure>

클래식 컬렉터들과 오늘날의 컬렉터들의 가장 큰 특징은, 신세대용, 구세대용 구분이 사라졌다는 점입니다. 즉, 어떤 조합이 최선일까 하는 고민을 하지 않아도 됩니다.

* 시리얼 컬렉터: 시리얼 올드를 흡수
* 페러렐: PS와 페럴렐 올드가 합쳐짐
* 페러렐 컬렉터: PS 컬렉터에서 옮겨감
* G1: CMS 자리를 차지
* 핫스팟의 가비지 컬렉터는 시리얼 -> CMS -> G1으로 발전해왔습니다.

JDK 21부터는 세대 구분 ZGC라고 하여, ZGC에 세대 구분 모드가 추가 되었습니다. 앞으로는 세대 구분 모드가 기본 설정으로 될 예정입니다.



## 저지연 가비지 컬렉터 (셰넌도어, ZGC)

불가능의 삼각 정리(impossible trinity. 가비지 컬렉터의 성능 측정 지표):

* 처리량
* 지연 시간
* 메모리 사용량

보통 세개 중 두가지만 달성 가능합니다. 세개는 불가능 할지도..

다만, 하드웨어 발전에 따라 메모리 사용량은 큰 문제가 되지 않습니다. 또한, 하드웨어 성능은 소프트웨어 시스템의 처리량에 직별되며, 사양 좋은 하드웨어를 쓰면 처리량이 늘어나게 됩니다.



<figure><img src="https://2804243633-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfV6clXG953LqFieNgWUf%2Fuploads%2FAfjahYzUPxbvq4mqL1Vc%2Fimage.png?alt=media&#x26;token=12140d46-09ec-47d5-b38f-2a4aa13f3ec1" alt=""><figcaption></figcaption></figure>

셰넌도어와 ZGC는 거의 모든 과정이 동시에 수행됩니다. 최초, 최종 표시에만 정지가 짧게 일어나고, 이 시간은 고정적입니다. (힙 크기와 객체 수가 많아지더라도 영향을 주지 않음)



## 셰넌도어

(셰넌도어는 오라클 JDK(유료)에는 없고, OpenJDK(오픈소스)에서만 존재)

개선 사항

1. 동시 모으기 자원
2. 세대 단위 컬렉터를 사용하지 않음
3. 기억 집합 대신 연결 행렬을 사용하여 참조 관계 기록

연결 행렬은 2차원 표로 이해할 수 있습니다.

<figure><img src="https://2804243633-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfV6clXG953LqFieNgWUf%2Fuploads%2F76UnRyHccmmbHZ0GhiNC%2Fimage.png?alt=media&#x26;token=787fbc71-d977-4e02-9dde-8ad13fef1533" alt=""><figcaption></figcaption></figure>

객체 A가 B를 참조하고 있으면, (5,3)에 표시를 하고, B가 C를 참조 하고 있으면 (3,1)에 표시를 합니다. 이걸 활용하여 리전 간 참조를 포함하는 리전들을 알아내게 됩니다.

<details>

<summary>기억 집합</summary>

* **다른 세대에 있는 객체를 참조하는 포인터 정보**를 따로 모아놓은 구조
* 특히 **Old 영역 → Young 영역**으로 가는 **cross-generation reference** 를 추적하기 위해 사용
* 이걸 안 하면, Young GC 할 때마다 Old 영역 전체를 훑어야 해서 성능이 떨어짐

</details>

<details>

<summary>연결 행렬</summary>

* **힙 메모리를 일정한 크기의 블록(Card)** 으로 나눈 다음,
* 각 카드의 **변경 여부(dirty 상태)** 를 기록한 테이블.

</details>



### 동작 방식

1. 최초 표시 (STW)
   1. 가장 먼저 GC 루트에서 직접 참조하는 객체들에 표시합니다.&#x20;
   2. 힙 크기와 상관없이 GC 루트 수에 따라 STW 발생
2. 동시 표시
   1. 객체 그래프를 타고 힙을 탐색하며 도달 가능한 모든 객체 표시
   2. 사용자 스레드와 동시 수행
   3. 사용자 스레드가 새로운 객체를 생성할 수 있으므로 힙 사용량이 늘어날 수 있음
3. 최종 표시 (STW)
   1. 보류 중인 모든 표시를 완료하고 GC 루트 집합을 다시 스캔.
   2. 회수 가치가 가장 큰 리전들을 추려 회수 집합을 생성

<details>

<summary>회수 집합</summary>

**회수 집합(Reclaim Set)** 은 GC(Garbage Collector)가 객체 마킹(marking)을 마친 뒤,\
“이제 이 영역은 필요 없어졌으니 회수(메모리 반환)할 수 있다”고 판단된\
**Region 목록 또는 객체 집합**을 의미

***

### 📍 어떤 상황에서 생길까?

특히 **G1 GC, ZGC, Shenandoah** 같은 GC들은 메모리를 **Region 단위**로 나눠 관리해.\
그래서 GC가 끝나면:

* 어떤 Region은 **살아있는 객체가 거의 없음**
* 어떤 Region은 **거의 다 비어 있음**

➡ 이런 Region은 굳이 남겨둘 이유가 없음

#### ✅ 그래서 GC는 :

> "이 Region들 안에 살아있는 객체가 별로 없으니까, 통째로 비워서 다시 사용할 수 있도록 회수하자!"

→ 이 **회수 대상 Region들**이 모인 게 바로 **Reclaim Set.** 살아있는 객체는 다른 곳으로 옮기고, Region은 비워버린다

</details>

4. 동시 청소
   1. 살아있는 객체가 하나도 없는 리전들을 청소
5. **동시 이주 (다른 컬렉터들과의 차이)**
   1. 회수 집합 안에 살아 있는 객체들을 다른 빈 리전으로 복사.
   2. 읽기 장벽과 포워드 포인터를 이용하여 사용자 스레드와 동시 수행
6. 최초 참조 갱신(STW)
   1. 힙에서 옛 객체를 가리키는 모든 참조를 복사 후의 새로운 주소로 수정
7. 동시 참조 갱신
   1. 참초 갱신을 실제로 수행
   2. 수행 시간은 메모리에 존재하는 참조의 수에 따름
   3. 물리 메모리 주소의 순서대로 참조 타입을 선형 검색하여 이전 값을 새로운 값으로 수정
8. 최종 참조 갱신(STW)
   1. GC 루트 집합의 참조 갱신
   2. 수행 시간은 GC 루트의 개수에 따름
9. 동시 청소
   1. 회수 집합의 모든 리전에는 살아있는 객체가 없기떄문에, 회수합니다.

정리:

<div data-full-width="true"><figure><img src="https://2804243633-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfV6clXG953LqFieNgWUf%2Fuploads%2F4d4JhZUnJ0sS750mFsB7%2Fimage.png?alt=media&#x26;token=f6500d09-d827-4495-96c9-a4bbb818bf8e" alt=""><figcaption></figcaption></figure></div>

<figure><img src="https://2804243633-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfV6clXG953LqFieNgWUf%2Fuploads%2F3XPFNXW1qxc0B4GfQsRe%2Fimage.png?alt=media&#x26;token=c76b69c2-ce77-42b6-8903-fdf4e1c7d66a" alt=""><figcaption></figcaption></figure>



### 동시 이주 과정

1. 회수 집합 안에 살아 있는 객체들을 다른 빈 리전으로 복사.
2. 읽기 장벽과 포워드 포인터를 이용하여 사용자 스레드와 동시 수행



메모리 보호 트랩(과거):

* 사용자 프로그램이 옛 객체가 저장된 메모리 공간에 접근하려 하면 트랩이 발동하여 미리 설정해 둔 예외 처리기가 실행.
* 그 후, 처리기에서 복사된 새 객체를 사용.
* 단점으로는, 운영 체제의 지원없이는 사용자, 커널모드 전환이 빈번하여 오버헤드 존재.



포워딩 포인터

* 각 객체의 헤더 앞에 포인터를 두는 방식(핸들 방식의 핸들 풀에 모아두는 방식과는 다름)
* +옛 객체의 포워딩 포인터가 새로운 객체를 가리키도록 수정
* -우회하여 접근하기 떄문에 모든 객체에오버헤드 발생.

<details>

<summary>핸들 방식 vs 포워딩 포인터 방식</summary>

핸들

\[코드] → \[핸들] → \[실제 객체]. 객체의 참조는 항상 **간접 참조**로 이루어짐 (깊이 2)

(간접이라 느림. 항상 간접이라 느림. )



포워딩 포인터

\[코드] → \[객체] ← 객체 헤더에는 자기 자신을 포인팅 (깊이 1)

\[코드] → \[옛 객체] → \[새 객체] ← (Read Barrier 또는 포워딩 포인터 따라감) (깊이 2)

(대부분 직접이라 빠름, 이동 시 간접 → 곧 다시 직접화)



</details>

다만, 포워딩 포인터는 스레드 경쟁이 발생.

1. GC 스레드가 객체에 복사본 생성
2. 사용자 스레드가 객체의 필드를 덮어씌움
3. GC 스레드가 엣 객체의 포워딩 포인터 값을 복사본의 주소로 수정.

이 부분은 CAS 기법(낙관적 락)을 사용하여 해결

<figure><img src="https://2804243633-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfV6clXG953LqFieNgWUf%2Fuploads%2FU8liAQIltfu3LWuVaboc%2Fimage.png?alt=media&#x26;token=b9d3fd6c-e5da-48ce-b2a7-b7079ce8fc48" alt=""><figcaption></figcaption></figure>

GC 스레드가복사 중에 사용자가 스레드가 쓰기를 시도할때,

<figure><img src="https://2804243633-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfV6clXG953LqFieNgWUf%2Fuploads%2FpEu34OCR2X8zbVdswrJU%2Fimage.png?alt=media&#x26;token=8cf41d6b-a2f3-4189-9d6a-0dfda7f35e60" alt=""><figcaption></figcaption></figure>

GC 스레드가 복사 중일때

[https://shipilev.net/talks/devoxx-Nov2017-shenandoah.pdf](https://shipilev.net/talks/devoxx-Nov2017-shenandoah.pdf) (p73, 74)



또한, 해시 계산, 객체 비교, 락 사용 상황에 문제가 발생할 수 있음.

1. 해시계산
   1. int hash = obj.hashCode();
   2. `obj`가 이주되었는데도, 여전히 예전 객체를 참조하고 있다면?
   3. 예전 객체에는 **올바른 필드 값이 없거나**, 이미 GC가 회수했을 가능성이 있음
2. 객체 비교
   1. if (obj1 == obj2) { }
   2. 만약 obj1은 새 주소, obj2는 포워딩 이전의 주소라면? 동일한 객체인데 다르게 인식
3. 락(lock) 사용
   1. synchronized(obj)
   2. 락을 거는 대상 객체가 포워딩된 상태면? JVM 내부 락 테이블이나 모니터 정보가 꼬일 수 있음

이 문제는 읽기, 쓰기 장벽으로 해결. 읽기는 빈번하기 때문에, 읽기 장벽을 최대한 가볍게 해야됨.

<details>

<summary>읽기, 쓰기 장벽</summary>

읽기

* 객체를 **읽을 때**
* 객체 이동 감지 / 리디렉션

쓰기

* 객체를 **쓸 때**
* 참조 변경 추적 / 기록

</details>



### 계속되는 개선(JDK 13)

* 로드 참조 장벽 도입:  객체 참조 타입의 데이터를 읽거나 쓸 때만 끼어드는 메모리 장벽 모델. (원시 타입은 간섭X)
* 포워딩 포인터를 객체 헤더에 통합: 마크 워드에서 0b11 플래그는 정의가 되지 않아있음. 이를 포워딩 포인터로 활용.
  * Mark Word에 `0b11`을 설정하고, 나머지 비트에는 **새로운 주소(New Location)** 를 저장함
  * 참조 형일때만(로드 참조 장벽으로 인해) 읽기 장벽 도입

<figure><img src="https://2804243633-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfV6clXG953LqFieNgWUf%2Fuploads%2FP0uNMbhE3dv8MjjuAvwu%2Fimage.png?alt=media&#x26;token=b2db5f6c-fb8f-402a-a625-9959b94f0871" alt=""><figcaption></figcaption></figure>

이로 인해, 포인터가 사용하던 5\~10% 메모리를 활용할 수 있음. 결론적으로 10\~15% 성능 개

* 같은 공간에 더 많은 객체를 담을 수 있음
* CPU 캐시에 더 많은 객체를 넣어서 캐시히트율을 높임
* 다른 가비지 컬렉터들과 객체 할당 코드를 공유할 수 있음



### 계속되는 개선2(JDK 17)

* 스택 워터마크를 활용한 스레드 스택 동시 처리
  * GC가 시작되면 GC는 모든 스레드의 스택을 스캔하여 참조들을 표시 큐에 담습니다. (안전 지대에서 세운 후 STW)
  * 도달 분석 알고리즘을 통해 참조 객체를 식별하고, 스택안에 참조들이 이동한 객체를 가리키도록 갱신합니다. (이떄도 STW)

이때, 위 과정에서 변화하는 부분은 최상위 스택 프레임뿐 (다른 프레임들은 이미 호출이 끝났거나 다음 호출을 기다리는 중이기 때문). "위"는 **동시 실행 중인, 즉 변화 중인 구간**이라 워터마크 아래부분만 GC가 건드리게 됩니다. "아래"는 참조가 고정되어 안전하게 스캔 가능.

<figure><img src="https://2804243633-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfV6clXG953LqFieNgWUf%2Fuploads%2FLLibOhvIpYpzNmnzLPC0%2Fimage.png?alt=media&#x26;token=a5d58204-8365-436b-ba58-7c25c5819de7" alt=""><figcaption></figcaption></figure>





## ZGC (JDK 15)

* JDK 15에 정식 버전, JDK 21부터는 신새대와 구세대를 구별하여 처리하는 세대 구분 ZGC가 추가.

### 리전 기반 메모리 레이아웃

* 동적으로 생성 파괴를 반복.
* 리전의 크기도 동적으로 달라짐

<figure><img src="https://2804243633-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfV6clXG953LqFieNgWUf%2Fuploads%2F0SB8rJb1f6h3PHe8rsFi%2Fimage.png?alt=media&#x26;token=b617ec96-3133-4a73-b6a4-50012871f822" alt=""><figcaption></figcaption></figure>

### 병렬 모으기와 컬러 포인터

컬러포인터는 **컬러 포인터는 병렬 모으기(parallel compaction)** 를 **효율적으로 구현하기 위한 테크닉.** 읽기 장벽을 사용하여 동시 이주를 구현.

헤더에 필드를 추가하여 추가 데이터를 저장했다. 하지만,

* 객체가 이동할 수 있는 환경에서 객체로의 접근이 반드시 성공할까?
* 객체에 직접 접근하지는 않지만 해당 객체와 관련된 데이터가 필요할때는?
  * 포인터가 가리키는 주소가 **더 이상 유효하지 않을 수도 있음**
  * 객체가 아직 **복사 중**이거나 **참조가 업데이트 안 됐을 수도 있음**

이때 두번째, 컬러 포인터는 객체 데이터가 필요할때를 위해 포인터 자체에 소량의 추가 정보를 직접 저장합니다.

<figure><img src="https://2804243633-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfV6clXG953LqFieNgWUf%2Fuploads%2FfEM85UTj5oQBB42PeZWY%2Fimage.png?alt=media&#x26;token=a2bcdc0e-84f0-418b-8ed7-954c62f234ea" alt=""><figcaption></figcaption></figure>

컬러 포인터 기술은:

* 주소 공간을 44 비트까지 제한
* 상위 4비트를 네 가지 플래그 정보를 저장하는데 이용.

가상 머신은 이 네 가지 플래그들을 통해 포인터만 보고도 객체의 삼색 표시 상태를 바로 알 수 있습니다. (해당 객체가 재매핑 집합에 추가 되었는지(이동), finalize() 메서드를 통해서만 접근할 수 있는지 알 수 있음)



컬러 포인터의 이점:

* 한 리전 안의 생존 객체들이 이동하면 그 즉시 해당 리전을 재활용할 수 있다. \
  -> 자가 치유때문에 참조들을 모두 수정할 때까지 기다릴 필요가 없다
* 가비지 컬렉션 과정에서 메모리 장벽의 수를 크게 줄일 수 있다.\
  -> 일부 기록 작업이 필요없어지기 때문
* 컬러 포인터를 객체 표시 및 재배치와 관련해 더 많은 정보를 담을 수 있는 확장 가능한 저장 구조로 쓸 수 있다.&#x20;



하지만, 컬러포인터는 자바 가상 머신이 메모리를 가리키는데 쓰는 포인터의 의미를 재정의하고, 운영 체제(프로세서)에서도 재정의된 대로 동작할지에 대한 여부가 문제가 됩니다.

**JVM이 포인터에 “컬러”를 붙여도**,\
이 포인터를 해석하는 **운영체제나 프로세서가 그 구조를 이해하지 못하면 문제가 생길 수 있다.**\
왜냐하면 **CPU는 포인터를 그저 “단순한 주소값”으로만 생각**

<details>

<summary>예시</summary>

JVM이 객체를 가리키는 포인터에 **색(상태) 정보 비트**를 붙이면:

```plaintext
plaintext복사편집0x0000_7FFF_FF00  ← 원래 객체 주소
0x0000_7FFF_FF01  ← 컬러 비트(01)가 붙은 포인터
```

이걸 JVM 내부에서는:

* 하위 2비트는 **색깔 정보**
* 나머지 비트는 **실제 주소**

이렇게 나눠서 쓸 수 있음

***

### 🧩 그런데! 문제는?

💥 **CPU나 운영체제는 이 포인터가 그런 식으로 구성되어 있는지 모른다!**

운영체제와 프로세서는 포인터를 그냥 \*\*“이건 메모리 주소야”\*\*라고만 봄

> 즉, 0x0000\_7FFF\_FF01은 그냥 주소 0x0000\_7FFF\_FF01로 해석됨.

그러니 **JVM이 “이건 색깔 정보야”라고 해석하길 원해도**,\
**CPU 입장에선 그냥 ‘그 주소를 가리키라는데?’ 하고 접근해버림** → 잘못된 메모리에 접근할 수 있음



프로세스를 마다 주소를 다르게 할당하였고, 프로세스A에서의 논리적 주소(01)는 A-01이고, 프로세스B에서의 주소(02)는 B-01인데, 01이라는 논리적 주소만으로 물리적 주소를 찾을 수 없습니다.

운영체제는 일반적으로 **포인터 구조를 모르고**,\
그저 각 프로세스에게 **가상 주소 공간**을 할당해줄 뿐

* **프로세스 A**: 논리 주소 0x01 → 실제 물리 주소 A-01
* **프로세스 B**: 논리 주소 0x01 → 실제 물리 주소 B-01

> 운영체제는 **“0x01”이라는 숫자가 어떤 의미로 쓰였는지 모르고**,\
> 단지 해당 프로세스의 **가상 주소 맵**을 기준으로 **페이지 테이블**을 통해 물리 주소로 변환해줌.

</details>



해결책??





### 동작 방식

<figure><img src="https://2804243633-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FfV6clXG953LqFieNgWUf%2Fuploads%2Fmiyuhl5Q7sseSTdkITqy%2Fimage.png?alt=media&#x26;token=9d6f7625-365a-445f-b1f4-3f6c8b5a5c76" alt=""><figcaption></figcaption></figure>

1. 동시 표시

동작 과정은 같지만, ZGC의 표시는 객체가 아니라 포인터에서 이루지고, 이때 컬러 포인터의 플래그가 갱신됩니다.

2. 동시 재배치 준비

청소할 리전들을 선정하여 재배치 집합을 만듭니다. ZGC는 모든 리전을 스캔하고, G1처럼 기억 집합을 관리하는 대신, 스캔을 광범위하게 하는 방식을 채택했습니다. 결국 재배치 집합안에 객체들을 다른 리전으로 복사한 후, 리전 자체를 회수할지 **여부만** 결정하게됩니다.\
-> 객체가 모두 죽은 리전을 알기때문에 재배치 집합에 없어도 해당 리전 회수 가능.

<details>

<summary>재배치 집합?</summary>

ZGC가 “이번에 객체를 이동시키겠다”고 선택한 리전(region)들의 모음.

**메모리 조각화(fragmentation)** 를 해결하기 위해\
살아있는 객체를 **다른 위치로 이주(evacuate)** 시키는 작업을 함

하지만 전체 힙에 있는 모든 객체를 옮기면 너무 비효율적이니까:

👉 "이번 GC 주기에서는 이 리전, 이 리전만 옮기자" 하고 **대상 리전만 추려서 작업**함.\
→ 이게 바로 **“재배치 집합(Relocation Set)”**, 즉 **재배치 대상임.**

</details>

3. 동시 재배치

재배치 집합 안의 생존 객체들을 새로운 리전으로 복사하는 과정. 이때, 각 리전의 포워드 테이블에 옛 객체와 새 객체의 이주 관계를 기록합니다.

컬러 포인터 덕에, ZGC는 객체가 재배치 집합에 속하는지 참조만 보고 알 수 있습니다. 사용자 스레드가 재배치 집합에 포함된 객체에 **처음 접**근하면 자가 치유 발생 (<-> 셰넌도어는 매번 접근할때마다 포워드 오버헤드 발생)

<details>

<summary>자가 치유</summary>

GC가 객체를 **이주(compaction)** 하는 중에도\
**사용자 스레드가 예전 주소로 객체를 참조하려 할 경우**,\
JVM이 그걸 **자동으로 감지하고**\
👉 **새로운 주소로 리디렉션 + 참조 갱신까지 해주는 기능**

**과정:**

* 사용자 스레드가 재배치 집합에 포함된 객체에 동시에 접근 시도

- 메모리 장벽이 해당 리전의 포워드 테이블에 기록된 정보를 보고 새로운 객체로 포워드.

* 동시에, 해당 참조 값도 새로운 객체를 직접 가리키도록 생신.

</details>

4. 동시 재매핑

힙 전체에서 재배치 집합에 있는 옛 객체들을 향하는 참조 전부를 갱신하는 작업. 자가 치유덕에, 굳이 필요가 없는 과정이고, 컬렉션 주기가 시작되는 동시 표시 단계와 통합할 수 있습니다.\
-> 동시 재매핑(포워딩된 객체의 참조를 새 주소로 갱신하는 단계)에서 힙 메모리를 다시 훑어야하기 때문에 동시 표시(어떤 객체가 살아있는지를 추적하는 단계)에서 힙 메모리를 훑는 과정과 통합이 가능합니다.



### 다른 컬렉터들과 비교

G1과 비교했을때, ZGC는:

* 기억 집합을 사용하지 않습니다\
  -> 메모리 사용량 감소
* 쓰기 장벽을 사용하지 않습니다\
  -> 오버헤드 감소
* 세대 구분이 없어서, 세대를 구분하기 위한 카드 테이블이 필요 없습니다.\
  -> 메모리 사용량 감소

<details>

<summary>세대 구분이 없기 때문에 문제점 발생</summary>

* 동시 회수를 진행하는 과정중, 애플리케이션 객체 할당 속도가 너무 빨라서 많은 객체를 새로 만들게 되면, 현 회수 단계에서는 다 표시하기 어려워서 대부분 살아남을 것.

- 현 회수 단계에서 살아남게되면 쓰레기 객체가됨.

* 다음 회수 과정중 애플리케이션이 또, 대량의 객체를 새로 또 만들게 되면, 동시 회수 주기가 계속해서 길어질 것.

- 결국 힙 여유 공간이 부족해짐

</details>

* NUMA 메모리를 고려한 메모리 할당\
  -> (NUMA 아키텍처가 적용된 환경에서는) 객체 생성을 요청한 스레드가 수행 중인 프로세서의 지역 메모리에 우선적으로 객체를 할당하여 메모리 접근 효율을 높임.





## 세대 구분 ZGC (JDK 21)

ZGC를 확장하여 신세대, 구세대를 구분하도록 개선. ZGC는항상 모든 객체를 대상으로 회수 작업을 진행해야했다는 점이 단점이었다.

* 세대간 참조를 효율적으로 추적하기 위해 쓰기 장벽 추가활용
* 컬러포인터에 새로운 메타데이터를 추가. \
  ->이 데이터는 쓰기 장벽이 발동할때, 현재 값이 써지는 필드가 세대 간 참조를 포함하는지 알 수 있습니다.
* 읽기 장벽에서 수행하던 도달 가능 객체 표시 작업을 쓰기 장벽으로 옮김.

<details>

<summary><strong>추가 내용</strong></summary>

"**세대형 ZGC(Generational ZGC)** 에서\
**도달 가능 객체 표시 작업(marking)을 기존의 "읽기 장벽(Read Barrier)"에서 "쓰기 장벽(Write Barrier)"으로 옮겼다"**



### ✅ 원문 요약

> **원래는** 읽기 장벽에서 “이 객체는 GC 루트에서 도달 가능한 객체야!” 라고 표시했는데,\
> **이제는** 그 작업을 쓰기 장벽으로 옮겨서 처리함\
> → 왜? 읽기보다 쓰기 횟수가 훨씬 적기 때문에, **성능 최적화 효과가 크다**

***

### ✅ 무슨 말이냐면?

ZGC나 Shenandoah 같은 **동시(marking) 기반 GC**에서는\
**GC 루트에서 도달 가능한 객체만 표시(mark)해야** 함.

이걸 어떤 방식으로 하냐면 보통:

#### 📌 예전 방식 (읽기 장벽 기반):

* 프로그램이 어떤 객체를 **읽을 때마다**
* 읽기 장벽이 개입해서
* “이 객체는 GC 루트에서 도달 가능하구나!” 하고 표시

→ 단점:\
**읽기는 너무 자주 발생함** → 성능 부담 큼

***

### ✅ 개선 방식 (쓰기 장벽 기반):

* 객체에 **새로운 참조를 쓸 때만**
* 즉, **어떤 객체 A의 필드에 새로운 객체 B를 할당할 때**
* 쓰기 장벽이 개입해서
* “B도 도달 가능하네!” 하고 표시

→ 장점:\
**쓰기 동작은 읽기보다 훨씬 적게 일어나므로, 오버헤드가 훨씬 작아짐**

***

### ✅ 세대형 ZGC에서 왜 이런 개선이 중요한가?

* 세대형 구조에서는 **Young → Old 참조** 같은 관계를 추적해야 하는데,\
  이를 위해 어떤 객체가 **도달 가능한가를 정확히 추적**해야 함
* 예전에는 **읽기 장벽(read barrier)** 로 처리했다면,
* 이제는 **쓰기 장벽(write barrier)** 로만 처리해도 충분히 정확하고,\
  💡 **성능도 훨씬 좋아지는 것.**

***

### ✅ 한 줄 요약

> **세대형 ZGC는 도달 가능 객체 표시 작업(marking)을 읽기 장벽에서 쓰기 장벽으로 옮겨,**\
> **성능 오버헤드를 줄이고 전체 GC 작업량을 줄이는 최적화를 적용했다.**

</details>



### 대표 기술

다중 매핑 메모리 제거

* 같은 힙 메모리를 세개의 독립된 가상 주소로 매핑
* ZGC에서는 컬러포인터가 있지만, 다중 매핑 메모리도 함께 사용했습니다.\
  -> 보조 수단으로 읽기/쓰기 접근 제어, 디버깅/접근 오류 감지/ 리디렉션 전후 안전성 확보

<details>

<summary>다중 매핑 메모리</summary>

"**같은 물리 메모리 블록을 서로 다른 가상 주소에 여러 번 매핑해서**,\
JVM이 같은 데이터를 **여러 방식으로 다르게 접근할 수 있게 하는 기법"**

ZGC에서는 객체가 다음 **세 가지 상태** 중 어디에 있는지를 구분해야 함:

| 상태                | 의미            | 주소 공간           |
| ----------------- | ------------- | --------------- |
| Marked (표시됨)      | 살아 있는 객체로 표시됨 | Marked 주소 영역    |
| Remapped (재매핑됨)   | 새 주소로 이동되었음   | Remapped 주소 영역  |
| Relocated (이주 완료) | 실제 이동된 객체     | Relocated 주소 영역 |

➡ 그래서 동일한 객체를,\
**마킹할 땐 marked 주소 공간에서**,\
**읽을 땐 remapped 주소 공간에서**,\
**최종 이주 위치는 relocated 주소 공간에서** 접근했음.

➡ 이걸 구현하려면, **동일한 물리 메모리를 3개의 가상 주소로 매핑**해야 했던 것.\
→ 이게 바로 "다중 매핑"

***

### ✅ 그런데 왜 제거했나?

다중 매핑은 다음과 같은 **복잡성과 단점**이 있어:

* **운영체제 의존성**: 플랫폼마다 메모리 매핑 방식이 다름
* **메모리 낭비처럼 보임**: 논리적으로는 메모리 하나지만, 주소 공간 3개 차지
* **디버깅/트래킹 어려움**: 동일한 데이터를 여러 주소로 접근 가능 → 혼란

➡ 그래서 **ZGC는 이후 버전부터 “다중 매핑 없이도 상태 구분이 가능하도록” 개선.**

#### 📌 대표적인 해결 방법?

* **컬러 포인터 (Colored Pointer)** 도입\
  → 포인터 하위 비트를 통해 상태 구분 (예: mark, remap, relocate 플래그)
* **주소 매핑 없이도, 포인터 자체로 어떤 상태의 객체인지 파악 가능**

</details>



다양한 장벽 최적화

* 기억 집합 장벽
* 시작 단계 스냅숏 표시 장벽
* 쓰기 장벽 버퍼
* 장벽 패치



이중 버퍼를 이용한 기억 집합 관리

* 세대간 포인터 관리에 기억 집합을 이용하지만, 카드 테이블을 활용하여, 해당 범위에 더렵혀졌다고 표시.
* 세대 구분 ZGC는 비트맵을 활용해서, 범위가 아닌 객체 필드 주소를 매핑.
* 구세대 리전 각각 두개의 집합 비트맵을 사용(사용자 스레드 수정용, GC 조회용).
* 결과적으로 사용자 스레드와 GC스레드는 서로 신경 쓰지 않고 각자 할일을 하게됨.



밀집도 기반 리전 처리

* "ZGC는 ‘GC로 인한 지연(latency)’을 줄이기 위해, 마킹(marking)을 무조건 끝까지 하지 않고, 상황에 따라 중간에 멈추기도 한다." (GC 스로틀링)
* 신세대에서 객체를 재배치할 때 살아 있는 객체 수는, 최근에 할당된 리전이라면 더 많이 살아 있을 가능성이 큼.\
  -> 리전에 더 많은 객체가 있을 수록, 해당 리전내에 객체가 살아있다 표시할 가능성이 높아짐.
* 밀도가 낮은 리전은 살아있는 객체가 있을 확률이 더 적으므로, 먼저 마킹.
* 살아있는 객체가 많으면 복사 과정이 더 오래 걸림

#### Dense heap regions <a href="#dense-heap-regions" id="dense-heap-regions"></a>

When relocating objects out of the young generation, the number of live objects and the amount of memory they occupy will differ across regions. For example, more-recently allocated regions will likely contain more live objects.

<mark style="color:red;">**ZGC analyzes the density of young-generation regions in order to determine which regions are worth evacuating and which regions are either too full or too expensive to evacuate**</mark>**.** The regions that are not selected for evacuation are aged in place: Their objects remain at their locations and the regions are either kept in the young generation as survivor regions or promoted into the old generation. The objects in the surviving regions get a second chance to die in the hope that, by the time the next young-generation collection starts, enough objects will have died to make more of these regions eligible for evacuation.

This method of aging dense regions in place decreases the effort required to collect the young generation.

[https://openjdk.org/jeps/439](https://openjdk.org/jeps/439)



거대 객체 처리

* 세대 구분 ZGC에서는 거대한 객체는 무조건 신세대에 할당합니다.
* 살아남은 거대한 객체를 구세대로 재배치하지 않고, 리전 자체를 노화시킬 수 있습니다.

<details>

<summary>GC 종류에 따른 할당, 관리</summary>

| GC 종류                 | 큰 객체는 어디로?             | 이유                             |
| --------------------- | ---------------------- | ------------------------------ |
| Serial, Parallel, CMS | Old                    | 복사 비용 줄이기 위해                   |
| G1GC                  | Humongous Region (Old) | Region 기반 처리                   |
| Shenandoah            | 조건에 따라 Young 또는 Old    | 필요에 따라 동적 결정                   |
| **ZGC**               | ✅ 항상 Young             | 금방 죽을 수도 있으니 빠른 회수 노림 + 승격 최적화 |



### 🔍 왜 GC마다 전략이 다를까?

| 이유             | 설명                                            |
| -------------- | --------------------------------------------- |
| ❗ **복사 비용 회피** | 큰 객체를 Young에 넣으면 → GC 시 복사 비용이 큼 (메모리, 시간 낭비) |
| ❗ **단편화 회피**   | 큰 객체는 공간 단편화 유발 위험 있음 → 별도 처리 필요              |
| ❗ **회수 최적화**   | 금방 죽는 객체는 Young에 있어야 빠르게 회수됨 → ZGC는 이걸 활용함    |
| ❗ **구조 차이**    | G1은 region 단위, ZGC는 region + 컬러 포인터 등 구조가 다름  |

</details>

