Chapter 7. 가비지 수집 고급
7.1 트레이드오프와 탈찰형 수집기
- 자바에 가비지 수집기가 있지만 자바/JVM 명세서에는 GC 구현 방법에 대해 일언반구도 없다.
- GC 기능이 없는 자바 구현체도 있다.
- 썬 마이크로시스템즈(오라클 전신) 환경 내부에서 GC는 탈착형 서브시스템으로 취급된다.
- 개발자는 GC 선정 시 다음 항목을 충분히 고민해야 한다.
- 중단 시간
- 처리율 (애플리케이션 런타임 대비 GC 시간 %)
- 중단 빈도
- 회수 효율 (GC 사이클 당 얼마나 많은 가비지가 수집되는가)
- 중단 일관성 (중단 시간이 고른 편인지)
- 최고 관심사는 ‘중단 시간’이지만 이것만 생각하면 안 된다.
- 고도 병렬 배치 처리 시스템 등에서는 중단 기간보다 처리율이 더 큰 영향을 미친다.
- 배치 잡에선 수십 초간 중단 시간 발생이 치명적이지 않기에 CPU 효율 및 처리율이 우수한 GC를 선택해야 한다.
- 성능 엔지니어는 GC 선정 시 다양한 트레이트오프와 관심사를 면밀히 검토해야 한다.
7.2 동시 GC 이론
- 메모리 할당은 불확정성을 유발하는 직접적 원인이기에 가비지 수집기는 중단 결정을 효과적으로 내리지 못한다.
- 최신 GC 이론은 불확정적 STW 문제 해결을 시도
- 동시 수집기를 사용하여 애플리케이션 스레드 실행 도중 수집에 필요한 작업 일부를 수행하는 것도 한 방법이다.
- 하지만 애플리케이션 처리 역량이 떨어지고 코드 로직은 더 복잡해진다.
7.2.1 JVM 세이프포인트
- 애플리케이션 스레드에는 세이프포인트라는 특별한 실행 지점이 존재한다.
- 세이프포인트는 스레드 내부 자료 구조가 훤히 보이는 지점
- 풀 STW가 필요할 때 GC 스레드가 OS에게 스레드를 강제 중단 요청을 할 방법은 없기에 애플리케이션 스레드는 반드시 서로 공조해야 한다.
- JVM은 다음 두 가지 규칙으로 세이프포인트를 처리한다.
- JVM은 강제로 스레드를 세이프포인트 상태로 바꿀 수 없다.
- JVM은 스레드가 세이프포인트 상태에서 벗어나지 못하게 할 수 있다.
- VM 인터프리터 구현체 어딘가에는 세이프포인트 요청을 받았을 때 그 지점에서 스레드 제어권을 반납하게 만드는 코드(배리어)가 있어야 한다.
- JIT 컴파일한 메서드에도 생성된 기계어 안에 이런 배리어가 꼭 들어가 있어야 한다.
- 세이프포인트 상태로 바뀌는 몇 가지 경우
- JVM이 전역 ‘세이프포인트 시간’ 플래그를 세팅
- 각 애플리케이션 스레드는 폴링하면서 이 플래그가 세팅됐는지 확인
- 애플리케이션 스레드는 일단 멈췄다가 다시 깨어날 때까지 대기한다.
- 세이프포인트 시간 플래그를 세팅하면 모든 애플리케이션 스레드는 반드시 멈춰야 한다.
- 일찍 멈춘 스레드는 늦게 멈춘 다른 스레드를 기다려야 한다.
- 스레드가 세이프포인트에 이르기까지 많은 시간이 소요될 수 있고 이론적으로는 절대 멈추지 않을 수도 있다.
- 자동으로 세이프포인트 상태가 되는 경우
- 모니터에서 차단된다.
- JNI 코드를 실행한다.
- 다음 경우엔 꼭 세이프포인트 상태가 되는 것은 아니다.
- 바이트코드를 실행하는 도중(인터프리티드 모드)
- OS가 인터럽트를 걸었다.
7.2.2 삼색 마킹
- 삼색 마킹 알고리즘 작동 원리 (회색: 처리 전, 검은색: 접근 가능 객체, 흰색: 수집 대상)
- GC 루트를 회색 표시한다.
- 다른 객체는 모두 흰색 표시한다.
- 마킹 스레드가 회색 노드로 랜덤하게 이동한다.
- 이동한 노드를 검은색 표시하고 이 노드가 기리키는 모든 흰색 노드를 회색 표시한다.
- 회색 노드가 하나도 남지 않을 때까지 위 과정을 되풀이한다.
- 검은색 객체는 모두 reachable한 것이므로 살아남는다.
- 흰색 노드는 접근 불가한 객체이므로 수집 대상이 된다.
- 동시 수집기에서는 삼색 마킹 알고리즘이 라이브 객체를 수집해버릴 수도 있다.
- 동시 수집은 SATB(snapshot at the beginning) 기법을 적극 활용한다.
- STAB: 수집 사이클을 시작할 때 접근 가능하거나 그 이후에 할당된 객체를 라이브 객체로 간주하는 것
- 즉 삼색 마킹 도중 애플리케이션 스레드가 흰색 객체를 새로 생성한 것 때문에 라이브 객체가 수집되는 현상이 발생할 수 있다.
- 동시 수집기에선 애플리케이션 스레드가 계속 객체 그래프를 변경하기 때문
- 이를 방지하기 위해선 추가적인 로직이 필요하다.
7.3 CMS
- CMS 수집기는 중단 시간을 짧게 하려고 설계된 테뉴어드 공간 전용 수집기이다.
- CMS는 중단 시간 최소화를 위해 애플리케이션 스레드 실행 중에 가급적 많은 일을 한다.
CMS 수행 단계
- 초기 마킹 (Initial Mark), STW
- ‘초기 마킹’ 단계의 목적은 해당 영역 내부의 확실한 GC 출발점(내부 포인터, 목적상 GC 루트와 같음)을 얻는 것이다.
- 동시 마킹 (Concurrent Mark)
- 삼색 마킹 알고리즘을 힙에 적용
- 나중에 조정해야 할지 모를 변경사항을 추적한다.
- 동시 사전 정리 (Concurrent Preclean)
- 재마킹 단계에서 가능한 STW 시간을 줄이는 것이 목표
- 재마킹 (Remark), STW
- 카드 테이블을 이용해 변경자 스레드가 동시 마킹 단계 도중 영향을 끼친 마킹을 조정한다.
- 동시 스위프 (Concurrent Sweep)
- 동시 리셋 (Concurrent Reset)
CMS를 워크로드에 적용했을 때의 효험
- 애플리케이션 스레드가 오래 멈추지 않는다.
- 단일 풀 GC 사이클 시간이 더 길다.
- CMS GC 사이클이 실행되는 동안 애플리케이션 처리율은 감소한다.
- GC가 객체를 추적해야 하므로 메모리를 더 많이 쓴다.
- GC 수행에 훨씬 더 많은 CPU 시간이 필요하다.
- CMS는 힙을 압착하지 않으므로 테뉴어드 영역은 단편화될 수 있다.
7.3.1 CMS 작동 원리
- 기본적으로 CMS GC 발생 시 가용 스레드 절반으로 GC 동시 단계를 수행하고, 절반은 자바 코드를 실행하는 데 쓰인다.
- 동시 실행 도중 새로운 객체가 할당될텐데 CMS 도중 에덴이 꽉 차버리면 어떻게 될까?
- 실행이 중단되고 CMS 도중 영 GC가 발생한다.
- 이 영 GC는 코어 절반만 사용(나머지는 CMS가 사용)하므로 병렬 수집기의 영 GC보다 더 오래 걸린다.
- 영 GC 이후 일부 객체는 테뉴어드로 승격되는데 CMS 수집기와 긴밀한 조정이 필요한 부분이라 CMS에선 조금 다른 영 수집기를 사용한다.
- 동시 모드 실패 (concurrent mode failure, CMF)
- CMS 도중 영 GC가 발생했을 때 할당률이 급증하면 많은 객체들이 테뉴어드로 조기 승격되고 테뉴어드 공간조차 부족해지는 사태가 발생하는 현상
- CMF 발생 시 JVM은 풀 STW를 유발하는 ParallelIOld GC 수집 방식으로 돌아간다.
- CMF가 자주 일어나지 않게 하려면 테뉴어드가 꽉 차기 전에 CMS 수집 사이클을 개시해야 한다.
- CMS가 시작하는 테뉴어드 힙 점유 수준은 기본 75%이고 스위치로 지정할 수도 있다.
- 힙 단편화 또한 CMF의 원인이다.
- 이러한 CMF를 방지하기 위해 CMS를 적용한 저지연 애플리케이션에서는 튜닝 자체가 주요 이슈이다.
- CMS는 내부적으로 프리 리스트를 이용해 사용 가능한 빈 공간을 관리한다.
- ‘동시 스위프’ 단계에서 스위퍼 스레드가 단편화로 인해 CMF가 발생하지 않도록 연속된 빈 블록들을 하나로 뭉친다.
- 스위퍼는 애플리케이션 스레드와 동시에 작동하므로 새로 할당된 블록이 잘못 스위프(청소)될 가능성이 있어 작업 도중 프리 리스트를 잠근다.
7.3.2 CMS 기본 JVM 플래그
- CMS 수집기는 다음 플래그로 작동한다.
- 최신 핫스팟 버전에서 이 플래그를 쓰면 ParNew GC(병렬 영 수집기를 조금 변형한 수집기)도 함께 작동한다.
7.4 G1
- G1의 특성
- CMS보다 훨씬 튜닝하기 쉽다.
- 조기 승격에 덜 취약하다.
- 대용량 힙에서 확장성(특히 중단 시간)이 우수하다.
- 풀 STW 수집을 없앨 수 있다. (또는 풀 STW 수집으로 되돌아갈 일을 확 줄일 수 있다.
오라클은 자바 9부터 G1을 디폴트 수집기로 정해 병렬 수집기를 대체하겠다고 밝혔다.
- G1은 병렬/CMS와 달리 세대마다 경계가 뚜렷한, 연속된 메모리 공간이 없고 반구형 힙 레이아웃 방식과도 전혀 무관하다.
7.4.1 G1 힙 레이아웃 및 영역
- G1 힙은 리전(region)으로 구성된다.
- 리전은 디폴트 크기가 1MB (힙이 클수록 커짐)
- 리전을 통해 세대를 북연속적으로 배치할 수 있고 GC 사이클마다 전체 가비지를 수집할 필요가 없다.
- G1 알고리즘에선 1, 2, 4, 8, 16, 32, 64MB 크기의 리전을 사용할 수 있다.
- 힙에는 2048 ~ 4095개의 리전이 있고 개수에 따라 리전 크기도 조정된다.
- 리전 크기 :
<힙 크기> / 2048
- 리전 개수:
<힙 크기> / <리전 크기>
- 이 값은 런타임 스위치로도 변경할 수 있다.
7.4.2 G1 알고리즘 설계
- G1 수집기가 하는 일
- 동시 마킹 단계 이용
- 방출 수집기
- ‘통계적으로 압착’
- G1은 워밍업 동안, GC가 한 번 돌 때마다 얼마나 많은 ‘일반’ 리전에서 가비지를 수집할 수 있는지를 수치로 보관한다.
- 제일 최근 GC 이후 새로 할당된 객체를 감당하기에 충분한 메모리를 수집할 수 있다면 G1은 할당보다 뒤쳐지지 않는 것
- TLAB 할당, 서바이버로 방출, 테뉴어드로 승격 등은 앞서 살펴본 다른 수집기와 대동소이하다.
리전을 절반 이상 점유한 객체는 거대 객체(humongous object)로 간주하여 ‘Humongous region(거대 리전)’이라는 별도 공간에 바로 할당한다. 거대 리전은 테뉴어드 세대에 속한 연속된 빈 공간이다.
- G1에선 세대를 구성하는 영역이 연속되어 있지 않다.
- 영 세대 크기는 전체 중단 시간 목표에 따라 조정된다.
- G1 수집기에는 ‘기억 세트(remembered set, RSet)’로 리전을 추적한다.
- 리전별로 하나씩 외부에서 힙 영역 내부를 참조하는 레퍼런스를 관리하기 위한 장치
- G1은 리전 내부 레퍼런스를 찾을 때 전체 힙을 다 뒤질 필요 없이 Rset만 꺼내 보면 된다.
- 병렬/CMS 수집기의 카드 테이블과 비슷하다.
- RSet, 카드 테이블 모두 부유 가비지(floating garbage)라는 GC 문제를 해결하는데 유용하다.
- 부유 가비지: 현재 수집 세트 외부에서 죽은 객체가 참조하는 바람에 죽었어야 할 객체가 계속 사는 현상
- 전역 마킹상으론 죽은 객체로 보이지만 범위가 제한적인 로컬 마킹상에선 살아있는 객체로 잘못 인식되는 것이다.
7.4.3 G1 단계
- G1의 수집 단계는 CMS 수집기와 비슷하다.
- 초기 마킹(STW)
- 동시 루트 탐색
- 초기 마킹 단계의 서바이버 영역에서 올드 세대를 가리키는 레퍼런스를 찾는 단계
- 반드시 다음 영 GC 탐색 시작 전에 끝내야 한다.
- 마킹 작업은 재마킹 단계에서 완료된다.
- 동시 마킹
- 재마킹(STW)
- 정리(STW)
- 어카운팅(accounting) 및 RSet ‘씻기(scrubbing)’을 수행하며 대부분 STW를 일으킨다.
- 어카운팅은 에덴에서 재사용 준비를 마친 영역을 식별하는 작업이다.
7.4.4 G1 기본 JVM 플래그
- 자바 8 이전엔 다음 스위치로 G1을 작동시킨다.
+XX:UseG1GC
- GC마다 애플리케이션 최대 중단 시간을 개발자가 지정할 수 있게 한 것
- 너무 낮게 잡으면 GC가 목표에 맞추지 못한다.
- 디폴트 중단 시간 목표를 200밀리초로 설정하는 스위치
- 중단 시간을 100밀리초 이하로 설정하면 현실성이 떨어지기에 디폴트 리전 크기값을 변경하는 방법도 있다.
-XX:G1HeapRegionSize=<n>
<n>
은 1~64까지의 2의 제곱수이다.
- G1은 안정된 알고리즘이지만 저지연 워크로드에서 아직 CMS보다 중단 시간이 긴 편이다.
7.8 레거시 핫스팟 수집기
- 이 밖에도 핫스팟 초기에는 다양한 수집기가 있었다.
- 운영계 용도로는 적합하지 않다.
- 자바 8에서 deprecated됐다가 자바 9부터 사용 금지된 수집기 조합도 존재한다.
7.8.1 Serial 및 Serial Old
- Serial/SerialOld GC는 병렬 수집기와 작동 원리는 같지만 CPU 코어 하나만 사용해 GC를 수행한다.
- 동시 수집이 안 되고 풀 STW를 일으킨다.
- 자바 8부터 deprecated된 수집기이지만 옛날 자바 버전으로 개발된 초창기 레거시 애플리케이션에서는 아직 쓰일 것이다.
7.8.2 증분 CMS(iCMS)
- 증분 CMS는 예전에 동시 수집을 시도했던 수집기
- 요즘 서버급 애플리케이션에서는 iCMS는 사용하지 않는게 좋다.
- 자바 9부터는 사라졌다.
7.8.3 디프리케이티드되어 사라진 GC 조합
- 지금까지 자바 GC는 특정 자바 버전에서 디프리케이티드 시켰다가 그 다음 버전부터는 아예 제거해버리는 식으로 진행 되었다.
- 아래 표는 자바 8부터 권장하지 않으며 자바 9에서는 제거된 GC 플래그 조합이다.
조합 |
플래그 |
DefNew + CMS |
-XX: -UseParNewGc -XX:+UseConcMarkSweepGC |
ParNew + SerialOld |
-XX:+UseParNewGC |
ParNew + iCMS |
-Xincgc |
ParNew + iCMS |
-XX:+CMSIncrementalMode -XX:+UseConcMarkSweepGC |
ParNew + iCMS |
-XX:+CMSIncrementalMode -XX:+UseConcMarkSweepGC -XX:-UseParNewGC |
CMS foreground |
-XX:+UseCMSCompactAtFullCollection |
CMS foreground |
-XX:+CMSFullGCBeforeCompaction |
CMS foreground |
-XX:+UseCMSCollectionPassing |
7.8.4 엡실론
- 엡실론 수집기는 레거시 수집기는 아니지만 어느 운영계 환경에선 절대 사용 금물인 수집기다.
- 엡실론은 테스트 전용으로 설계된 아무 일도 안하는 시험 수집기이다.
- 실제로 가비지 수집 활동을 일체 하지 않는다.
- JVM은 결국 언젠가 메모리가 고갈되어 멎게 된다.
- 엡실론 수집기가 유용한 경우
- 테스트 및 마이크로벤치마크 수행
- 회귀 테스트
- 할당률이 낮거나 0인 자바 애플리케이션 또는 라이브러리 코드의 테스트
- JMH 테스트를 할 때 GC 이벤트를 확실히 배제할 수 있어 좋다.
- VM-GC 인터페이스를 개발하는 과정에서 인터페이스 자체를 점검하는 최소한의 테스트 케이스를 수립할 때 엡실론은 유용하다.