3. 가비지 컬렉터와 메모리 할당 전략
- 이 책에서 ‘메모리 할당과 회수’할때의 메모리는 힙메모리
3.2 대상이 죽었는가?
3.2.1 참조 카운팅 알고리즘
- 직접 가비지 컬렉션
- 객체를 가리키는 참조 카운터를 두는 방식, 일반적으로 사용됨 python, rust,,
- JVM에서는 사용하지않음
- 순환참조인 경우 참조카운팅이 1과 1이므로, 두 객체 모두 사용되지 않음에도 GC가 걷어갈 수 없음.
3.2.2 도달 가능성 분석 알고리즘
- 간접 가비지 컬렉션
- 어떤 객체와 GC 루트 사이를 이어주는 참조 체인이 없다면 회수대상.
3.2.3 다시 참조이야기
- 강한 참조 : 프로그램 코드에서 참조를 할당하는것, 관계가 남아있다면 절대 회수하지 않는다.
Object obj = new Object()
- 부드러운 참조 : 메모리가 부족하여 오버플로우가 나기 직전에 두번째 회수를 위한 회수목록에 추가된다.
SoftReference
클래스
- 약한 참조 : 다음 GC까지만 살아있다. 메모리가 넉넉하더라도 회수된다.
WeakReference
클래스
- 파이널 참조 :
finalize()
를 구현한 객체, 모든 참조가 사라지면 fianlize 메서드가 호출된다.- JDK9부터 사라진 스펙, 몰라도 됨.
- finalize 에서 자신을 참조하면, GC에서 1회 면제될 수 있다. 꼼수이기도하고, 불확실해서 제거되었다고함.
- 유령 참조 : 객체 수명에 아무런 영향을 주지않는다. 유일한 목적은 대상 객체가 회수될 때 알림을 받기 위함.
PhantomReference
클래스
3.2.5 메서드 영역 회수하기
- JDK12 이후의 ZGC 부터 클래스 언로딩을 지원한다.
- 메서드 영역에서 GC가 회수하는것은 상수와 클래스.
- GC 효율이 heap에 비해 떨어지는편
- 클래스 회수
Xnoclassgc
옵션 지원- 리플렉션, 동적 proxy, CGLib 같은 바이트코드 프레임워크를 많이 사용하는 경우, JSP를 동적으로 생성하고 클래스 로더를 자주 사용자화하는 OSGi 환경 등에서는 일반적으로 타입 언로딩을 지원해야한다.
3.3 가비지 컬렉션 알고리즘
3.3.1 세대단위 컬렉션 이론
- GC에서 살아남은 횟수로 자바 힙 내에서 다른 영역에 할당하는 방식
- 실제 상황에서 얻은 경험 법칙을 구현한 것이다.
- 약한 세대 가설 : 대다수의 객체는 일찍 죽는다
- 강산 세대 가설 : GC 과정에서 살아남은 횟수가 늘어날수록 더 오래 살 가능성이 커진다.
- 세대간 참조 가설 : 세대간 참조의 갯수는 같은 세대 안에서의 참조보다 훨씬 적다.
- 신세대 young : 생명주기가 짧은 객체, 한번도 GC에서 살아남지않은 객체,
- 구세대 old : GC에서 신세대 객체가 다수 죽고, 소수가 구세대로 승격된다.
- 두 세대간에 참조관계를 가지는 객체가 있다면, young -> old 로 승격한다.
- 세대간 참조관계를 알기위해 old를 minor GC마다 볼수없으므로, old를 기억집합이라는 조각단위로 나누어 참조가 있는지 별도로 기록하고, GC때에는 해당 조각만 본다.
GC 방식
- 부분 GC
- minor GC (young GC) : 신세대만 대상
- major GC (old GC) : 구세대만 대상. (CMS 컬렉터만 구세대를 따로 회수한다.)
- 혼합 GC : 신세대 전체와 구세대 일부 대상. (G1 컬렉터만 이렇게 동작.)
- Full GC : 자바 힙 + 메서드 영역
3.3.2 mark-sweep 알고리즘 (기본)
- 회수대상을 mark 하고, sweep 단계에서 회수
- 단점
- 객체가 많아질수록 표시하고 쓸어담는 작업의 효율이 떨어진다.
- sweep 이후 불연속적인 메모리 파편화가 심하다.
3.3.3 mark-copy 알고리즘
- mark-sweep 단점 2를 보완
- 가용 메모리를 같은 크기의 두 블록으로 나누어, 한쪽블록이 꽉차면 살아남는 객체를 copy, 기존 블록을 청소한다.
- 단점
- 가용 메모리가 절반이 된다.
- 객체 생존률이 높아질수록 복사할게 많아져서 효율이 나쁘다.
- 대다수가 살아남는 old 영역에서는 적합하지 않음.
- 아펠스타일 : young 영역을 나누는 방식.
- 에덴 (80%) : 신규 객체 영역
- 생존자 공간 1(10%) : 한번 GC 후 살아남은 영역 (예비 old?)
- 생존자 공간 2(10%) : <에덴 + 생존자공간1> 이 한번에 GC되어 살아남은 객체를 이곳에 할당. GC 이후에는 <에덴 + 생존자공간2> 가 사용된다.
- 생존자공간 10%가 GC 이후 수용하지 못하는 경우, old 에 바로 추가된다.
- stop-the-world가 발생하는 mark-compact 대신, 지연시간에 중점을 둔 다면 mark-copy 가 유리. (CMS collector)
3.3.4 mark-compact 알고리즘
- mark-copy의 단점 2를 보완
- compact 단계에서 생존객체를 한쪽 끝으로 모으고, 그외 메모리 청소.
- 참조를 바꾸는것은 stop-the-world 를 유발.
- 구세대에서 생존객체가 많을때에는 참조 이동이 부담될것.
- 프로그램 처리량관점에서는 메모리 할당에 유리한 mark-compact가 유리. (parellel old collector)
3.4 핫스팟 알고리즘 상세 구현
3.4.1 루트 노드 열거
- 도달 가능성 분석 알고리즘에서 GC root 집합으로부터 참조 체인을 찾는 작업.
- 루트 노드들의 참조 관계가 변하지 않아야하므로, stop-the-world는 불가피.
- 거의 정지하지않는 CMS,G1,ZGC도 이때는 일시정지를 피할 수 없다.
- 가상머신이 객체 참조가 저장된 위치를 직접 알아내는 방법도 있다.
- Hotspot 은 OopMap 데이터 구조를 사용하여, JIT 컴파일과정에서 네이티브 코드상에 스택의 위치, 어느 레지스터 데이터가 참조인지 기록한다. 이 정보로 루트 직접 얻기 가능.
3.4.2 안전 지점
- HotspotVM이 참조관계나 OopMap 내용을 변경하는 명령어에도 OopMap를 생성하면 공간비용이 드므로, Safe point 라는 위치에만 기록한다.
- GC는 안전지점이 도달할때까지는 stop-the-world하지않는다.
- (자발적 멈춤) 일반적으로 중단 flag bit를 polling하여 확인.
- 안전 지점의 위치는 메서드 호출,순환문,예외처리 같은 명령어 흐름 다중화 하는 명령어에서 생성한다.
안전 지점에만 OopMap을 저장한다는건줄 알았는데, 코드 3-4를 보아 안전지점은 참조가 변경되지않는 스레드 중단 지점인듯하고, 메서드마다 다 저장되는것같은데???
3.4.3 안전 지역
- 일정 코드 영역에서는 참조관계가 변하지 않음을 보장하는 영역이다.
- sleep 중이거나, 스레드가 block된 실행중이지않은 프로그램인 경우 안전지점까지 수행하는걸 기다릴 수 없기때문에 생성된 개념.
- 안전 지역안에서는 GC를 언제든 시작해도 무방하다.
- 스레드가 안전지역을 벗어날때 stop-the-world 중이라면, 중지 해제 신호를 기다려야한다.
3.4.4 기억집합과 카드 테이블
- 비회수 영역에서 회수 영역을 가리키는 포인터들을 기록하는 추상 데이터 구조.
- 정밀도
- 워드 정밀도 : 레코드 하나가 메모리 워드(32/64bit) 하나에 매핑.
- 객체 정밀도 : 레코드 하나가 객체 하나에 매핑. (비회수 영역 객체에 마킹되는개념?)
- 카드 정밀도 : 레코드 하나가 메모리 블록 하나에 매핑.
- 카드 테이블 : 카드 정밀도로 구현된 기억집합, 가장 널리 쓰임.
- Hotspot에서는 byte배열로 구현한다. 카드테이블에서의 1개의 비트가 2^9 byte 크기의 메모리 블록(카드페이지)의 참조 여부 플래그를 가지고있다.
CARD_TABLE[A address >> 9] = 1; // A 객체가 위치한 메모리 블록에 있는 객체들 중 회수대상을 참조하는 것이 있다.
- Hotspot에서는 byte배열로 구현한다. 카드테이블에서의 1개의 비트가 2^9 byte 크기의 메모리 블록(카드페이지)의 참조 여부 플래그를 가지고있다.
3.4.5 쓰기 장벽
- 카드테이블 갱신 시점은 참조 타입 필드에 값이 대입되는 순간이다.
- JIT 컴파일 후의 캐싱된 코드는 기계어 명령어이므로, JVM이 명령어를 개입하기 어렵다.
- HotSpot에서는 참조타입 필드 대입 후에 (사후 쓰기 장벽) 카드테이블 갱신 메서드를 호출하는 코드를 추가 후 컴파일한다.
- 거짓 공유 : 동시성 문제, 동일 CPU 프로세서 캐시라인에 해당하는 카드테이블을 동시에 수정할때 동기화문제가 발생할수있는데, 캐시라인만 같을뿐 실제로는 공유하고있지 않지만 동시성영향을 줄 수 있기때문에 거짓 공유라고 함.
3.4.6 동시 접근 가능성 분석
- 루트노드 열거 단계에서 GC 루트는 전체 자바힙에 존재하는 객체대비 매우 적은양이며, OopMap같은 최적화 덕에 루트노드열거시 중단 시간은 매우 짧고 일정하다.
- GC루트에서 객체 그래프를 탐색하는 과정은 객체 수에 비례한다.
- 객체 그래프 탐색시에 중지가 발생해야하는 이유 : 살아있는 객체가 죽었다고 표시되어 회수될 수 있다.
- 중단없이 살아있는 객체가 청소되는 문제 해결방법
- 증분 업데이트 : 탐색이 끝난 노드에서 새로운 참조가 생기면 별도 기록
- 시작 단계 스냅숏 : 노드 탐색이 시작하는 순간을 기준으로 스캔하고, 탐색중에 참조가 제거되면 별도 기록.
- 핫스팟 VM은 1,2를 모두 사용.
중단없이 참조 변경 사항을 계속 기록하면 이것도 끝이 없지 않을까 ?.?
3.5 클래식 가비지 컬렉터
3.5.1 serial collector
- 신/구세대 지원
- 단일스레드로 동작하는 GC
- 회수가 완료될때까지 stop-the-world
- 신세대 : mark-copy
- 구세대 : mark-compact
- 장점
- 다른 단일 스레드 알고리즘보다 간단하고 효율적
- 메모리 사용량이 가장 적다
- 코어수가 적은 환경이라면 스레드 상호작용에 의한 오버헤드가 없다.
- -XX:+UseSerialGC
3.5.2 ParNew collector
- 신/구세대 지원
- 시리얼 컬렉터를 병렬화 한 버전
- 구세대 CMS + 신세대 ParNew 조합으로 인기가 높았다. (시리얼 컬렉터도 조합가능)
- G1GC가 힙 전체를 대상으로 하는 컬렉터로 등장하면서 CMS에 통합되었다.
- 프로세서별
- 단일 코어 프로세서에서는 시리얼 컬렉터보다 성능이 떨어진다.
- 가상 듀얼 코어 환경에서도 시리얼 컬렉터보다 낫다고 보장할 수 없다.
- 코어수가 늘어나면 시스템 자원을 효율적으로 사용할 가능성이 커진다. (GC 스레드수는 기본적으로 코어수와 동일)
병렬과 동시 개념의 컬렉터
- 병렬 parallel : GC 스레드 다수가 동시에 수행
- 동시 concurrent : GC 스레드와 사용자 스레드가 동시에 일을 진행
3.5.3 PS (parellel scavenge) collector
- 신세대용
- 처리량을 제어하는것이 목표이다.
- CMS 는 사용자 스레드의 일시정지 시간을 최소로 줄이는것이 목표였음.
- 처리량 : 사용자 코드 실행시간 비율 = (사용자 코드 실행시간) / (사용자 코드 실행 시간 + GC 실행 시간)
- 100분중 GC에 1분이 소요되었다면 처리량은 99%
- 응답속도를 보장해야하는 QoS 프로그램 등에서 사용하기 좋음.
- GC 정지시간의 최댓값
-XX:MaxGCPauseMillis
처리량 지정-XX:GCTimeRatio=(Int)
- 정지시간을 줄여버리면, 신세대의 크기가 작게 할당되고, 더 자주 회수해야해서 처리량이 낮아진다
- 적응형 조율 전략 : 처리량을 조정하면, VM이 성능 모니터링 정보를 수집하여 최적의 정지시간과 신세대의 크기 등을 자동으로 조절해준다.
- 세밀한 수동 최적화가 어렵다면 PS컬렉터가 괜찮은 선택
-XX:GCTimeRatio=99
: 애플리케이션이 GC보다 99배이상의 시간을 써야한다 = GC가 전체 실행시간의 1%를 초과하지 않아야한다.
3.5.4 serial old collector
- 시리얼 컬레겉의 구세대용 버전
- ~JDK6까지 신세대 PS + 구세대 serial old 조합으로 사용됨.
3.5.5 parallel old collector
- 처리량이 중요하거나 프로세서 자원이 부족한 상황 : JDK7~ 신세대 PS + 구세대 Parallel old
- old 메모리 용량이 크고, 하드웨어가 상대적으로 우수한 상황 : 신세대 Parnew + 구세대 CMS
3.5.6 CMS collector
- mark and sweep 을 사용자 스레드와 동시에 수행한다.
- 이 컬렉터의 목적은 일시정지 시간을 최소로 줄이는것.
- 웹서버에서 사용시 사용자 경험을 개선시킬 수 있다.
- 과정
- 최초 표시 (mark) : 아주 빠르게 정지, GC 루트 마크
- 동시 표시 (mark) : 사용자 스레드와 동시 동작. 객체 탐색
- 재표시 (mark) : 1보다는 길게 정지, (3.4.6 증분업데이트 참고)
- 동시 슬기 (sweep) : 사용자 스레드와 동시 동작.
- 단점
- 동시수행이므로, 프로세서 자원에 매우 민감하다.
- 사용자 스레드를 멈추지는 않더라도, 애플리케이션을 느리게하고 전체 처리량을 떨어뜨린다.
- 부유쓰레기를 처리하지못해 동시모드 실패 유발 가능성.
- 부유 쓰레기 : 재표시에서 놓친 객체는 다음 GC에서 회수된다.
- 동시 실행되므로, CMS는 구세대가 가득 찰때까지 여유롭게 기다릴 수없다.
- 메모리 문턱값을 조정할 수 있는데, 그럼에도 메모리가 다 차버리면 동시모드 실패. 임시로 serial old collector를 실행하며 전체 중지된다.
- mark-sweep 이므로 메모리 파편화가 심하면 전체 GC시 참조객체 이동이 필요하다. 이때 중지시간이 길어진다.
- 동시수행이므로, 프로세서 자원에 매우 민감하다.
- JDK9부터 폐기대상, JDK14에서 제거됨.
3.5.7 G1 GC (가비지 우선 Garbage First Collector)
- JDK9 : PS + ParallelOld 를 밀어내고 서버모드용 기본 컬렉터가 되미.
- JDK10 : GC 인터페이스를 도입하여 관심사분리
-
JDK 14 : CMS가 F/O
- G1의 목표는 정지시간 예측 모델
- 기존 GC와의 차이 : 크기와 수가 고정된 세대 단위 영역 구분에서 벗어나, 연속된 자바 힙을 동일 크기의 여러 독립 리전으로 나누고, 이 리전중 이득이 큰 영역을 회수영역으로 고른다.
- G1이 해결해야했던 문제
- 리전간 참조문제 : 기억집합을 도입하여 참조문제를 해결하지만, 양방향 테이블이 필요햐여 기존 GC보다 메모리를 많이 사용한다.
- 재표시 단계 : 시작단계 스냅샷 알고리즘을 사용 (CMS는 증분 업데이트 알고리즘)
- 신뢰할 수 있는 정지 시간 예측 모델 : 감소평균 (최근의 평균적인 상태를 더 정확하게 알 수 있음)을 사용.
- 과정
- 최초 표시 (mark) : 아주 빠르게 정지. GC 루트 마크, TAMS 포인터의값 수정(= 새로운 객체는 가용리전에 할당되며, 이는 시작 스냅샷 생성과 동일한 효과)
- 동시 표시 (mark) : 사용자 스레드와 동시 동작. 객체 탐색
- 재표시 (mark) : 매우빠르게 정지함. 시작단계 스냅샷이후 변경된 소수의 객체만 처리.
- 복사 및 청소 (copy, sweep) : 리전을 회수가치와 비용에 따라 줄세우고, 목표한 일시정지 시간에 부합하도록 회수 계획을 세운다.
- 리전에서 살아남은 객체들은 빈 리전에 이주시키는데, 이때 일시 중지되어야한다.
- G1의 최우선 목표가 짧은 지연시간이 아닌 예측가능성 이므로, 일시정지하여 회수 효율을 극대화 하는 방향으로 구현되어있다.
- 기본 목표 정지시간은 200ms (일반적으로 100~300ms 가 적정)
- 정지시간이 매우 짧다면, 그 여파로 회수 속도가 새로 할당되는 속도를 따라잡지 못하여 쓰레기가 쌓여갈것이고, 종국에는 Full GC가 발생할것.
3.5.8 오늘날의 GC
- 그림 3-14 참고
- 신세대용과 구세대용의 구분이 사라졌다는 특징.
3.6 저지연 가비지 컬렉터
- 가비지 컬렉터 불가능의 삼각정리 : 최대 두가지만 달성가능하다.
- 지연시간
- 처리량 : 하드웨어의 성능에 따라 올릴수있음
- 메모리 사용량 : 하드웨어의 성능에 따라 올릴 수 있음
3.6.2 셰넌도어
- 오라클에서 만들지않아 OpenJDK에서만 사용가능
- 목표 : 힙 크기와 상관없이 GC로 인한 일시정지를 10ms 이내로 하는것.
G1 GC와의 차이점
- 동시이주 지원
- G1 : 복사 단계에서 일시정지가 불가피하며, 병렬로 수행하여 시간단축.
- 셰넌도어 : 사용자 스레드와 동시 수행.
- 신세대와 구세대 리전을 구별하지 않는다.
- 세대구분의 개발 우선순위가 낮았을 뿐, JDK21 이후 세대단위 컬렉션은 생김.
- 기억집합 대신 연결 행렬
- G1 : region간 참조를 기억집합에 저장하여 메모리 사용량 증가
- 셰넌도어 : 2차원 표 처럼 저장.
동작 방식
- 최초 표시 (mark) : 일시정지. GC 루트 마크
- 동시 표시 (mark) : 객체 탐색
- 최종 표시 (mark) : 일시정지. GC루트집합 다시 스캔.
- 동시 청소 (sweep) : 살아있는 객체가 없는 region 청소
- 동시 이주 (copy) : 살아있는 객체들을 다른 region 으로 복사.
- 참조 수정시 동시성 보장을 위해 읽기장벽과 포워딩 포인터를 이용한다.
- 최초 참조 갱신 : 일시정지. 스레드 집결지를 설정해 동시이주단계가 끝났음을 보장하는 역할.
- 동시 참조 갱신 : 참조하는 객체의 주소 수정.
- 물리 메모리 주소의 순서대로 참조타입을 선형 검색하여 이전 값을 새로운 ㄱ밧으로 수정한다.
- 최종 참조 갱신 : 일시정지. GC 루트집합의 참조 갱신.
- 동시 청소 (sweep) : 이주된 region 청소
포워딩 포인터, 동시 이주의 핵심
- 사용자 스레드와 동시에 참조하는 객체의 주소를 바꾸는 방법.
- 기존에는 메모리에 ‘메모리 보호 트랩’ 을 설정하여 새 객체를 이용하게 하는 방법이 있었다. 하지만 운영체제의 지원없이는 비용이 큼.
- 객체 레이아웃 구조 상단에 참조필드 하나를 추가하여, 처음에는 객체 자신을 가리키도록 설정, 주소가 바뀌면 새로운 객체 주소를 가리키도록 수정.
- 단점
- 포인터를 거쳐 객체에 접근하는 오버헤드, 실행시간 비용이 있음.
- 객체 주소 변경 전까지는 구/신 객체의 동기화가 필요한데, CAS 기법으로 해결함.
- CAS : TODO
- ‘객체로의 접근’에 속하는 동작을 모두 제어하려면 쓰기/읽기장벽 모두 사용해야했는데, 읽기장벽에 동작이 많아지면 치뤄지는 비용이 컸다.
계속되는 개선
- JDK13~ 로드참조 장벽 도입 : 객체 참조 타입의 데이터를 읽거나 쓸때만 끼어드는 메모리 장벽 모델
- JDK13~ 포워딩 포인터를 객체 헤더에 통합 : 마크워드의 마지막 2비트를 포워딩 포인터로 활용하여 메모리 절약
- GC 수행횟수 절감
- CPU 캐시 적중률 높아짐
- 다른 GC와 객체헤더 구조가 같아 구현로직이 단순해짐.
- JDK17~ 스택 워터마크를 활용한 스레드 스택 동시 처리
- 스레드 스택중 변화가 생기는 부분은 최상위 스택 프레임이다. (pop,push)
- 최상위 스택 프레임에 스택 워터마크를 표시하고, 최상위 프레임은 pop 될때(안전지대를 벗어났을때 , 스택 워터마크를 한칸 내려야할때) 스캔한다.
- 그 외의 스택 프레임은 GC가 동시 스캔할 수 있다.
실전 성능
- 셰넌도어의 총 정지시간이 G1 대비 11s -> 0.32s , 정지횟수는 26 -> 6
- 셰넌도어의 ‘최장 정지시간을 10ms 이내로 제어하겠다’는 목표에는 부합하지 못함.
- 셰넌도어는 JDK8까지도 지원한다.
3.6.2 ZGC
- JDK15 에 정식버전 출시
- JDK21~ 세대구분 ZGC 추가.
ZGC는 세대구분 없이 리전 기반 메모리 레이아웃을 사용한다. 낮은 지연 시간을 최우선 목표로 하며, 동시 마크-컴팩트 알고리즘을 구현하기 위해 읽기 장벽, 컬러 포인터, 메모리 다중 매핑 기술을 이용하는 가비지 컬렉터이다.
리전기반 메모리 레이아웃
- ZGC의 리전은 동적으로 생성/파괴된다.
- small : 2MB로 고정, 256KB 미만의 작은 객체
- medium : 32MB로 고정, 256KB~4MB 미만의 객체
- big : 2MB의 배수, 하나의 큰 객체만 담는다. medium보다 작을 수 있다.
병력 모으기와 컬러 포인터(=태그 포인터, 버전 포인터)
- 컬러포인터 : 포인터 자체에 소량의 추가 정보를 직접 저장하는 기술.
- 플래그
- marked0, marked 1 : GC의 마킹(표시)단계에서 사용된다. (회수대상 마킹), GC사이클마다 번갈아가면서 사용하며 사이클을 구분한다.
- remapped : 참조가 최신 상태이고, 객체의 현재위치를 정확하게 나타내는지를 의미한다.
- fianlizable : finalize() 메서드를 통해서만 접근가능한지 여부.
- 64bit 리눅스 프로세스가 사용할 수 있는 물리 주소 공간은 46bit(max 64TB) 이며, 64TB 메모리는 충분한 양이므로, 일부를 플래그정보 저장에 사용한다.
- 64bit = 16(사용안함) + 4(flag) + 44(객체주소)
- 주소공간이 줄어서, 최대 2^44 = 16TB 메모리를 활용할 수 있다.
- 위 이유로 32bit 플랫폼에서는 컬러포인터를 사용할 수 없다.
- 객체 도달 가능성 분석 방식 차이
- G1, 셰넌도어 : 객체그래프를 순회
- ZGC : 참조 그래프를 순회.
ZGC 장점
- A region -> B resion 생존 객체 copy가 완료되면, A region을 바로 재사용할 수 있다.
- G1, 셰넌도어는 copy 후 참조테이블을 전체 수정해야했는데, 자가치유방식으로 이를 개선했다.
- 자가치유
- 옛 객체에 처음 접근할 때만 포워드가 일어나며, 참조값을 갱신한다. (remapped 플래그)
- 참조변경을 위한 메모리 (읽기/쓰기) 장벽 중 ZGC는 읽기장벽만 사용하여 처리량을 높인다.
- 컬러포인터의 앞 16bit는 사용되지않으므로, 성능 개선 잠재력이 있다.
- 가상메모리로 다루는 주소공간이 실제 메모리 용량보다 크다.
- ps 로 메모리 확인하면 실제보다 3배높게 측정된다.
- 세대구분 ZGC부터 제거되었다.
컬러포인터의 다중매핑 메모리 주소지정 그림 3-25 이해못함
- NUMA 메모리를 고려한 메모리 할당
- NUMA : 멀티코어 프로세서를 탑재한 컴퓨터를 위해 설계된 메모리 아키텍처.
- 객체생성을 요청한 스레드가 수행중인 프로세서의 지역 메모리에 우선적으로 객체를 할당하여 메모리 접근 효율을 높인다.
ZGC의 동작 방식
- 표시 시작
- GC 루트가 직접 가리키는 객체 표시
- 동시 표시
- G1, 셰넌도어와 달리 포인터에 표시한다.
- Marked0, Marked1 플래그가 이 표시단계에서 갱신된다.
- 동시 재배치 준비
- GC마다 모든 리전을 스캔하여, 청소해야할 리전을 선정, 재배치 집합을 만든다.
- 동시 재배치
- 생존 객체를 새로운 리전으로 복사.
- 구 리전 포워드 테이블에 옛 객체 -> 새 객체 이주관계를 기록한다.
- 컬러포인터의 markedx 플래그로 재배치 집합 여부를 참조만 보고 알 수 있다.
- 재배치 후 자가치유로 참조값 갱신이 동시에 일어난다.
- 동시 재배치 후 해당리전을 즉시 재활용 가능하며, 포워드 테이블은 회수되면안된다.
- 동시 재매핑
- 힙 전체에 옛 객체들을 향하는 참조 전부를 갱신한다.
- 자가치유되지않은 객체들을 모두 갱신하고, 포워드 테이블을 삭제할 수 있다.
- ZGC는 시급하지않은 작업으로, 다음 GC의 동시표시단계와 통합했다.
다른 컬렉터들과의 비교
|x|G1|ZGC|세대구분 ZGC| |—-|—|—|—| | 세대,리전간 참조 관계 파악 | 카드테이블 | 컬러포인터 | 컬러포인터 (메타데이터 추가), 기억집합| | 참조 처리방식 | 쓰기장벽(기억집합 관리) , 읽기장벽 (포워딩 포인터) | 읽기장벽(포워딩 포인터 1회 = 자가치유)| 쓰기(기억집합)/읽기(포워딩포인터) 장벽| | 처리율 성능 순위 | 3 |2|1|
- 세대단위 컬렉션의 필요성
- GC로 회수되는 메모리공간 < GC기간동안 만들어지는 객체 = 힙의 여유공간이 계속 줄어들것.
- 객체 할당 속도를 높이고싶다면, 세대단위 컬렉션을 도입해야한다.
3.6.3 세대 구분 ZGC
- JDK21~
java -XX:+UseZGC -XX:+ZGenerational
- 세대구분 없던 ZGC 대비 처리량이 크게 늘었다. (카산드라는 4배증가)
다중 매핑 메모리 제거
- 실제 메모리 사용량을 파악할 수 있다. (ps 명령어)
- 컬러포인터에서 다중 매핑 관련 메타데이터 비트제거로, 다른 용도로 메모리를 활용할 수 있다.
다양한 장벽 최적화
이중버퍼를 이용한 기억 집합 관리
- 일반적으로 카드테이블의 1bit = 힙의 512byte 에 대응하는 플래그였는데
- ZGC는 1bit = 1객체의 필드주소를 뜻한다.
- 2개의 기억집합을 가지고 CUD/R 용도를 분리한다.
밀집도 기반 리전 처리
- 신세대 리전의 밀집도에따라 회수리전 우선순위를 정한다. 오래된 신세대 리전일수록 밀집도가 높아질것이고, 회수대상이 많을 것.
거대 객체 처리
- 거대 객체도 신세대에 바로 할당한다. GC 후 살아남는다면 객체 재배치 없이 리전을 구세대로 승격한다.