Garbage Collection은 시스템에 있는 객체들의 수명을 정확히 몰라도 런타임이 대신 객체를 추적하며 쓸모없는 객체를 제거하는 것이다.
GC는 Pluggable Subsystem으로 고려되며 JVM Spec 에도 객체용 힙 곤강은 자동 저장소 관리 시스템으로 회수한다. 어떤 일이 있어도 객체를 명시적으로 해제해서는 안된다
라고만 적혀있다.
즉 같은 자바 프로그램 이라도 코드 변경 없이 다양한 GC에서 돌려 볼 수 있다.
모든 GC 구현체는 아래와 같은 두가지 기본 원칙을 준수해야한다.
또한 Weak Generational Hypothesis
는 JVM 메모리 관리의 이론적 근간을 형성하였다.
STW(stop-the-world)
GC 사이클이 발생하여 가비지를 수집하는 동안 모든 애플리케이션 스레드가 중단된다. 즉 GC를 실행하는 스레드르르 제외한 나머지 스레드는 모두 작업을 멈춘다.
(대개의 경우 GC 튜닝이란 이 STW 시간을 줄이는 것이다.)
동시
GC 스레드는 애플리케이션 스레드와 동시(병행) 실행 될 수 있다. 이는 계산 비용 면에서 아주 어렵고 비싼 작업인 데다 실상 100% 동시 실행을 보장하는 알고리즘은 없다.
(e.g: CMS, Concurrent Mark Sweep는 준 동시(mostly concurrent) 수집기)
병렬
여러 스레드를 동원해서 가비지를 수집한다.
(Parellel GC, Parellel Old GC)
정확
정확한 GC 스킴은 전체 가비지를 한방에 수집할 수 있게 힙 상태에 관한 충분한 타입 정보를 지니고 있다.
보수
보수적인 스킴
은 스킴의 정보가 없다. 그래서 리소스를 낭비하는 일이 잦고 근본적으로 타입 체계를 무시하기 때문에 훨씬 비효율적이다.
이동
이동 수집기에서 객체는 메모리를 여기저기 오갈 수 있다. 즉 객체 주소가 고정된게 아니다(C++처럼 raw pointer로 직접 엑세스X)
압착
할당된 메모리(즉 살아남은 객체들)는 GC 사이클 마지막에 연속된 단일 영역 배열되며, 객체 쓰기가 가능한 여백의 시작점을 가리키는 포인터가 있다.
앞착 수집기는 메모리 단편화(memory fragmentation)를 방지한다.
방출
수집 사이클 마지막에 할당된 영역을 완전히 비우고 살아남은 객체는 모두 다른 메모리 영역으로 이동(방출)한다.
GC Root는 메모리의 Anchor Point로 메모리 풀 외부에서 내부를 가리키는 포인터이다. 예를들면 아래와 같은 종류가 있다.
Mark And Sweep
알고리즘은 할당됐지만 아직 회수되지 않은 객체를 가리키는 포인터를 포함한 할당 리스트(allocated list)를 사용한다.
Mark And Sweep
또한 Compaction
는 메모리 단편화를 방지하기 위해 파편화된 메모리 영역을 앞에서부터 채워나가는 작업을 의미한다.
Mark-Sweep-Compaction 작업은 아래와 같은 메모리 변화를 나타낸다.
HotSpot은 몇 가지 메커니즘을 응용하여 Weak Generational Hypothesis
를 활용한다.
Generational Count
(객체가 지금까지 무사 통과한 가비지 수집 횟수)를 센다.Eden
공간에 생성한다.Survivor
영역 중 하나로 이동된다. Old(or Tenured)
에 보관한다. Generational Collection
또한 Hotspot은 Old 영역에 있는 객체가 Young 영역의 객체를 참조할때는 카드 테이블(card table)이라는 자료구조에 정보를 기록한다.
여기서 카드 테이블은 JVM이 관리하는 바이트 배열로 각 원소는 Old 세대 공간의 512 바이트 영역을 가리킨다.
카드 테이블을 통해 Young 영역의 GC를 실행할 때는 Old 영역에 있는 모든 객체의 참조를 확인하지 않고, 이 카드 테이블만 조회하여 GC 대상인지 식별한다.
늙은 객체 o에 있는 참조형 필드값이 바뀌면 o에 해당하는 instanceOop가 들어 있는 카드를 찾아 해당 엔트리를 Dirty 마킹한다.
Hotspot은 필드를 업데이트할 때마다 단순 Write Barrier
를 이용한다. 여기서 Write Barrier
는 늙은 객체와 젊은 객체의 관계가 맺어지면 카드 테이블 엔트리를
더티 값으로 세팅하고, 반대로 관계가 해제되면 더티 값을 지우는 실행 엔진에 포함된 작은 코드 조각이다.
cards[*instanceOop >>9] = 0;
이란 코드는 카드에 Dirty 하다고 표시한 값이 0이고 카드 테이블이 512 바이트라서 9비트 우측 시프트 연산을 한것이다.
멀티 스레드 환경에서 Thread-Safe하게 Eden 영역에 객체를 저장하려면 락이 발생할 수 밖에 없고 lock-contention 때문에 성능은 매우 떨어 질 것이다.
그래서 JVM은 Eden
영역을 여러 버퍼로 나누어 각 애플리케이션 스레드가 새 객체를 할당하는 구역으로 활용하도록 배포한다.
또한 Hotspot은 애플리케이션 스레드에 발급한 TLAB 크기를 동적으로 조정한다. 또한 bump-the-pointer를 이용하여 마지막 객체(Eden의 Top)를 추적한다.
그러면 생성되는 객체가 있으면 해당 객체의 크기가 Eden 영역에 넣기 적당한지만 확인 후 할당된다.
-XX:+UseSerialGC
Old 영역의 GC는 Mark-Sweep-Compact 이라는 알고리즘을 사용한다. Old 영역에 살아 있는 객체를 식별(Mark)한 뒤 Heap의 앞 부분부터 확인하며
살아 있는 것만 남긴다(Sweep). 마지막 단계에서는 각 객체들이 연속되게 쌓이도록 힙의 가장 압 부분부터 채워서 객체가 존재하는 부분과 객체가 없는 부분
으로 나눈다(Compaction)
-XX:+UseParallelGC
Serial GC와 기본적인 알고리즘은 같으나 GC를 처리하는 스레드가 여러 개이다.
(출처: https://d2.naver.com/helloworld/1329)
-XX:+UseParallelOldGC
현재(자바 8 기준) 디폴트 올드 세대 수집기이다. 위의 Parallel GC와 비교하여 Old 영역의 GC 알고리즘만 다르다. Mark-Summary-Compaction 단계를 거친다.
동시 수집기를 이용하여 애플리케이션 스레드의 실행 도중 수집에 필요한 작업 일부를 수행한다. 그러면 STW 시간을 줄일 수 있다.
물론 그만큼 실제 애플리케이션 작업에 투입 가능한 처리 역량을 빼앗기고 수집하는 코드 로직은 한층 더 복잡해진다.
JVM은 조정 작업을 위해 애플리케이션 스레드마다 Safepoint 라는 특별한 실행 지점을 둔다. 여기서 어떤 작업을 하기 위해 해당 스레드는 잠시 중단이 될 수 있다.
예를들어 풀 STW 가비지 수집의 경우 안정된 객체 그래프가 필요하므로 애플리케이션 스레드를 반드시 중단시켜야한다. GC 스레드가 OS에게 무조건 애플리케이션 스레드를
강제 중단할 방법이 없기 때문에 스레드간의 공조가 필요하다. JVM은 아래와 같이 두 가지 규칙에 따라 Safepoint를 처리한다.
따라서 세이프포인트 요청을 받았을 때 그 지점에서 제어권을 반납하게 만드는 코드(배리어)가 VM 인터프리터 구현체 어딘가에 있어야 한다(JIT 포함).
삼색 마킹은 객체를 흰색(메모리 해제 해야 할 객체), 회색(Root에서 접근 가능하지만, 이 객체에서 가리키는 객체들은 아직 검사하지 않음), 검정색(Root에서 접근 가능하고 흰색 객체를 가리키지 않음)으로 분류한다.
삼색 마킹 알고리즘의 작동 원리는 아래와 같다.
삼색 알고리즘을 실행하는 도중에 애플리케이션 스레드가 계속 객체 그래프를 변경할 수 있다. 필요한 락킹 개수 등 성능 기준에 따라 삼색 마킹 문제를 해결하는
방법은 수집기마다 다르다.
-XX:+UseConcMarkSweepGC
CMS GC는 중단 시간을 아주 짧게 하려고 설계된 Tenured 공간 전용 수집기이다. 보통 영 세대 수집용 병렬 수집기(Parallel GC)를 조금 변형한 수집기(ParNew)와 함께 쓰인다.
CMS GC는 아래와 같은 단계를 거친다. 1(초기 마킹)과 4(재마킹) 동안은 모든 애플리케이션 스레드를 멈추고 나머지 단계에서는 애플리케이션 스레드와 병행하여 GC를 수행한다.
CMS GC는 아래와 같은 장단점이 있다.
-XX:+UseG1GC
G1(Garbage First)은 병렬 수집기이며 CMS와는 전혀 스타일이 다르다. G1은 아래와 같은 특성을 가지고 있다.
G1의 힙 레이아웃 및 영역은 아래와 같다.
G1 수집기는 올드 객체가 영 객체를 참조하는 걸 추적하기 위해 기억 세트(RSet, remembered set)를 이용한다. RSet는 영역별로 하나씩 존재하며 외부에서 힙 영역 내부를 참조하는 레퍼런스를 관리하기 위한 장치이다.
G1 수집기는 아래와 같이 작업이 수행된다.