JVM Garbage Collection(GC)

zaccoding·2021년 1월 5일
0

Table of Contents


GC 개요

Garbage Collection은 시스템에 있는 객체들의 수명을 정확히 몰라도 런타임이 대신 객체를 추적하며 쓸모없는 객체를 제거하는 것이다.

GC는 Pluggable Subsystem으로 고려되며 JVM Spec 에도 객체용 힙 곤강은 자동 저장소 관리 시스템으로 회수한다. 어떤 일이 있어도 객체를 명시적으로 해제해서는 안된다 라고만 적혀있다.

즉 같은 자바 프로그램 이라도 코드 변경 없이 다양한 GC에서 돌려 볼 수 있다.

모든 GC 구현체는 아래와 같은 두가지 기본 원칙을 준수해야한다.

  1. 알고리즘은 반드시 모든 가비지를 수집해야 한다.
  2. 살아 있는 객체는 절대로 수집해선 안된다.

또한 Weak Generational Hypothesis는 JVM 메모리 관리의 이론적 근간을 형성하였다.

  1. 대부분의 객체는 금방 접근 불가능 상태(unreachable)가 된다.
  2. 오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다.


GC 용어

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

GC Root는 메모리의 Anchor Point로 메모리 풀 외부에서 내부를 가리키는 포인터이다. 예를들면 아래와 같은 종류가 있다.

  • 스택 프레임(Stack Frame)
  • JNI
  • 레지스터(hoisted variable)
  • (JVM 코드 캐시에서) 코드 루트
  • 전역 객체
  • 로드된 클래스의 메타데이터


Mark And Sweep

Mark And Sweep 알고리즘은 할당됐지만 아직 회수되지 않은 객체를 가리키는 포인터를 포함한 할당 리스트(allocated list)를 사용한다.

  1. 할당 리스트를 순회하면서 마크 비트(mark bit)를 지운다.
  2. GC 루트부터 살아 있는 객체를 찾고 마크 비트를 세팅한다.
  3. 할당 리스트를 순회하면서 마크 비트가 세팅되지 않은 객체를 찾는다.
    3.1 힙에서 메모리를 회수해 프리 리스트(free list)에 되돌린다. 3.2 할당 리스트에서 객체를 삭제한다.

Mark And Sweep

또한 Compaction는 메모리 단편화를 방지하기 위해 파편화된 메모리 영역을 앞에서부터 채워나가는 작업을 의미한다.

Mark-Sweep-Compaction 작업은 아래와 같은 메모리 변화를 나타낸다.



Generational Collection

HotSpot은 몇 가지 메커니즘을 응용하여 Weak Generational Hypothesis를 활용한다.

  1. 객체마다 Generational Count(객체가 지금까지 무사 통과한 가비지 수집 횟수)를 센다.
  2. 큰 객체를 제외한 나머지 객체는 Eden 공간에 생성한다.
  3. GC가 한 번 발생한 후 살아남은 객체는 Survivor 영역 중 하나로 이동된다.
  4. 충분히 오래 살아남은 객체들은 별도의 메모리 영역 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비트 우측 시프트 연산을 한것이다.



TLAB(Thread Local Allocation Buffer)

멀티 스레드 환경에서 Thread-Safe하게 Eden 영역에 객체를 저장하려면 락이 발생할 수 밖에 없고 lock-contention 때문에 성능은 매우 떨어 질 것이다.

그래서 JVM은 Eden 영역을 여러 버퍼로 나누어 각 애플리케이션 스레드가 새 객체를 할당하는 구역으로 활용하도록 배포한다.

또한 Hotspot은 애플리케이션 스레드에 발급한 TLAB 크기를 동적으로 조정한다. 또한 bump-the-pointer를 이용하여 마지막 객체(Eden의 Top)를 추적한다.

그러면 생성되는 객체가 있으면 해당 객체의 크기가 Eden 영역에 넣기 적당한지만 확인 후 할당된다.



Serial GC

-XX:+UseSerialGC

Old 영역의 GC는 Mark-Sweep-Compact 이라는 알고리즘을 사용한다. Old 영역에 살아 있는 객체를 식별(Mark)한 뒤 Heap의 앞 부분부터 확인하며

살아 있는 것만 남긴다(Sweep). 마지막 단계에서는 각 객체들이 연속되게 쌓이도록 힙의 가장 압 부분부터 채워서 객체가 존재하는 부분과 객체가 없는 부분

으로 나눈다(Compaction)


Parallel GC

-XX:+UseParallelGC

Serial GC와 기본적인 알고리즘은 같으나 GC를 처리하는 스레드가 여러 개이다.

(출처: https://d2.naver.com/helloworld/1329)


Parallel Old GC

-XX:+UseParallelOldGC

현재(자바 8 기준) 디폴트 올드 세대 수집기이다. 위의 Parallel GC와 비교하여 Old 영역의 GC 알고리즘만 다르다. Mark-Summary-Compaction 단계를 거친다.



동시 GC 이론

동시 수집기를 이용하여 애플리케이션 스레드의 실행 도중 수집에 필요한 작업 일부를 수행한다. 그러면 STW 시간을 줄일 수 있다.

물론 그만큼 실제 애플리케이션 작업에 투입 가능한 처리 역량을 빼앗기고 수집하는 코드 로직은 한층 더 복잡해진다.

JVM 세이프포인트

JVM은 조정 작업을 위해 애플리케이션 스레드마다 Safepoint 라는 특별한 실행 지점을 둔다. 여기서 어떤 작업을 하기 위해 해당 스레드는 잠시 중단이 될 수 있다.

예를들어 풀 STW 가비지 수집의 경우 안정된 객체 그래프가 필요하므로 애플리케이션 스레드를 반드시 중단시켜야한다. GC 스레드가 OS에게 무조건 애플리케이션 스레드를

강제 중단할 방법이 없기 때문에 스레드간의 공조가 필요하다. JVM은 아래와 같이 두 가지 규칙에 따라 Safepoint를 처리한다.

  1. JVM은 강제로 스레드를 세이프포인트 상태로 바꿀 수 없다.
  2. JVM은 스레드가 세이프포인트 상태에서 벗어나지 못하게 할 수 있다.

따라서 세이프포인트 요청을 받았을 때 그 지점에서 제어권을 반납하게 만드는 코드(배리어)가 VM 인터프리터 구현체 어딘가에 있어야 한다(JIT 포함).

  1. 스레드가 자동으로 세이프포인트 상태가 되는 경우
  • 모니터에서 차단된다.
  • JNI 코드를 실행한다.
  1. 아래와 같은 경우 꼭 세이프포인트 상태가 되는건 아니다.
  • 바이트코드를 실행하는 도중(인터프리티드 모드)이다.
  • OS가 인터럽트를 걸었다.

삼색 마킹

삼색 마킹은 객체를 흰색(메모리 해제 해야 할 객체), 회색(Root에서 접근 가능하지만, 이 객체에서 가리키는 객체들은 아직 검사하지 않음), 검정색(Root에서 접근 가능하고 흰색 객체를 가리키지 않음)으로 분류한다.

삼색 마킹 알고리즘의 작동 원리는 아래와 같다.

  1. GC 루트를 흰색으로 표시한다.
  2. 다른 객체는 모두 흰색으로 표시한다.
  3. 마킹 스레드가 회색 노드로 랜덤하게 이동한다.
  4. 이동한 노드를 검은색으로 표시하고 이 노드가 가리키는 모든 흰색 노드를 회색으로 표시한다.
  5. 회색 노드가 하나도 남지 않을 때까지 위 과정을 반복한다.
  6. 검은색 객체는 모두 Reachable 한 것으로 살아남는다.
  7. 흰색 노드는 더 이상 접근 불가한 객체이므로 수집 대상이 된다.

삼색 알고리즘을 실행하는 도중에 애플리케이션 스레드가 계속 객체 그래프를 변경할 수 있다. 필요한 락킹 개수 등 성능 기준에 따라 삼색 마킹 문제를 해결하는

방법은 수집기마다 다르다.



CMS

-XX:+UseConcMarkSweepGC

CMS GC는 중단 시간을 아주 짧게 하려고 설계된 Tenured 공간 전용 수집기이다. 보통 영 세대 수집용 병렬 수집기(Parallel GC)를 조금 변형한 수집기(ParNew)와 함께 쓰인다.

CMS GC는 아래와 같은 단계를 거친다. 1(초기 마킹)과 4(재마킹) 동안은 모든 애플리케이션 스레드를 멈추고 나머지 단계에서는 애플리케이션 스레드와 병행하여 GC를 수행한다.

  1. 초기 마킹(Initial Mark) (STW)
    (클래스 로더에서 가장 가까운 객체 중 살아 있는 객체만 찾는 것으로 끝낸다. 그래서 STW가 매우 짧다.)
  2. 동시 마킹(Concurrent Mark)
    (삼색 마킹 알고리즘을 힙에 적용하면서 나중에 조정해야 할지 모를 변경 사항을 추적한다.)
  3. 동시 사전 정리(Concurrent Preclean)
    (재마킹 단계에서 가능한 STW 시간을 줄이는 것이 목표다.)
  4. 재마킹(Remark) (STW)
    (카드 테이블을 이용해 변경자 스레드가 동시 마킹 단계 도중 영향을 끼친 마킹을 조정한다.)
  5. 동시 스위프(Concurrent Sweep)
    (쓰레기를 정리하는 작업을 실행한다.)
  6. 동시 리셋(Concurrent Reset)

CMS GC는 아래와 같은 장단점이 있다.

  1. 애플리케이션 스레드가 오랫동안 멈추지 않는다.
  2. 단일 풀 GC 사이클 시간이 더 길다.
  3. CMS GC 사이클이 실행되는 동안 애플리케이션 처리율은 감소한다.
  4. GC가 객체를 추적해야 하므로 메모리를 더 많이 쓴다.
  5. GC 수행에 더 많은 CPU 시간이 필요하다.
  6. CMS는 힙을 압착하지 않으므로 Tenured 영역은 단편화 될 수 있다.
  7. 할당률이 급증하여 영 수집 시 조기 승격이 일어나 Tenured 공간이 부족하면 JVM은 풀 STW를 유발하는 ParellelOld GC 방식으로 돌아간다.

G1

-XX:+UseG1GC

G1(Garbage First)은 병렬 수집기이며 CMS와는 전혀 스타일이 다르다. G1은 아래와 같은 특성을 가지고 있다.

  1. CMS보다 훨씬 튜닝하기 쉽다.
  2. 조기 승격에 덜 취약하다.
  3. 대용량 힙에서 확장성(특히 중단 시간)이 우수하다.
  4. 풀 STW 수집을 없앨 수(또는 풀 SATW 수집으로 되돌아갈 일이 확 줄일 수 ) 있다.

G1의 힙 레이아웃 및 영역은 아래와 같다.

  • G1 힙은 영역(Region)으로 구성되어 있다.
  • 영역은 디폴트 크기가 1메가 바이트(힙이 클수록 커짐)이다.
  • G1 힙은 메모리상에서 연속돼 있지만, 각 세대를 구성하는 메모리를 더 이상 연속해서 배치할 필요가 없다는 뜻이다.
  • 거대영역(humongous region)은 테뉴어드 세대에 속한 연속된 빈 공간이며 영역의 절반 이상을 점유한 객체는 거대 객체로 간주하여 거대영역에 할당된다.

G1 수집기는 올드 객체가 영 객체를 참조하는 걸 추적하기 위해 기억 세트(RSet, remembered set)를 이용한다. RSet는 영역별로 하나씩 존재하며 외부에서 힙 영역 내부를 참조하는 레퍼런스를 관리하기 위한 장치이다.

G1 수집기는 아래와 같이 작업이 수행된다.

  1. 초기 마킹(STW)
    (서바이버 영역에서 올드 세대를 가리키는 객채들을 Mark)
  2. 동시 루트 탐색
    (초기 마킹 단계의 서바이버 영역에서 올드 세대를 가리키는 레퍼런스를 찾는 동시 단계로 반드시 다음 영 GC 탐색을 시작하기 전에 끝내야한다.)
  3. 동시 마킹
  4. 재마킹(STW)
  5. 정리(STW)
    (Accounting 및 RSet scrubbing 태스크를 수행하며 대부분 STW를 일으킨다.)

profile
초보 서버 개발자 :)

0개의 댓글