Java GC

강다빈·2024년 7월 16일

java

목록 보기
1/1

참고1
참고2
참고3
참고4

stop the world

  • GC을 실행하기 위해 JVM이 애플리케이션 실행을 멈추는 것.
  • GC 실행 스레드를 제외한 나머지 스레드는 모두 작업을 멈춘다. → 작업 완료 이후에야 중단했던 작업을 다시 시작한다.
  • GC 튜닝 = stop-the-world 시간을 줄이는 것

명시적 메모리 해제

Java는 프로그램 코드에서 메모리를 명시적으로 지정하여 해제하지 않는다.

  • 해당 객체를 null로 지정
  • System.gc() 호출 → 시스템의 성능에 매우 큰 영향을 미치므로 절대 사용하지 말것 ⚠️

Garbage Collector가 더 이상 필요 없는 객체를 찾아 지우는 작업을 한다.

Weak generational hypothesis

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

이 가설의 장점을 최대한 살리기 위해 HotSpot VM(JVM)에서는 Young/Old 영역으로 물리적 공간을 나누었다.

  • Young(Young Generation) 영역 : 새롭게 생성한 객체의 대부분이 위치하는 곳. 금방 접근 불가능 상태가 되기 때문에 매우 많은 객체가 이 영역에 생성되었다가 사라진다.
    • 객체가 사라진다 = Minor GC가 발생한다
  • Old 영역(Old Generation) : Young 영역에서 살아남은 객체가 여기로 복사된다.
    • 대부분 young영역보다 크게 할당하며, 크기가 큰 만큼 young 영역보다 GC는 적게 발생한다.
    • MajorGC(Full GC)가 발생 = 객체가 사라짐

  • Permanent Generation : Method Area라고도 함.

    • 객체나 억류(intern)된 문자열 정보를 저장하는 곳
    • 여기서 GC가 발생하면 Major GC 횟수에 포함된다.
    • Java8에선 metaspace로 교체되었다고 한다.
      • 현재까지 로드한 class들의 metadata가 저장되는 공간
      • JVM이 아닌 OS에서 관리하는 Native 메모리(시스템의 기본 메모리) 영역
      • default로 제한된 크기를 갖고 있지 않고, 필요한 만큼 늘어남
      • java8부턴 permgen 관련 jvm 옵션 무시
  • Card Table : Old 영역에 있는 객체가 young 영역의 객체를 참조하는 경우가 있을 때 처리하는 곳

    • oldgen의 메모리 512바이트 당 1바이트의 카드를 가지고 있다
    • old에서 young을 참조할 때마다 정보가 표시된다.
      • 시작주소에 해당하는 card를 dirty로 표시한다.
      • ref가 해제되면 dirty 표시가 사라진다.
    • young 영역의 GC를 실행할 때에는 old 영역에 있는 모든 객체의 참조를 확인하지 않고, 이 카드 테이블만 뒤져서 GC 대상인지 식별한다.
      • 늘어놓고 뒤집어가며 참조의 존재 여부를 파악할 수 있게 해주는 장치
    • Write Barrier를 사용하여 관리 → Minor GC를 빠르게 할 수 있도록 하는 장치

      A write barrier in a garbage collector is a fragment of code emitted by the compiler immediately before every store operation to ensure that (e.g.) generational invariants are maintained.

      • 약간의 오버헤드는 발생하나 전반적인 GC 시간은 줄어들게 된다.

Young 영역의 구성

객체가 제일 먼저 생성되는 Young 영역은 3개의 영역으로 나뉜다.

  • Eden
  • Survivor(2개)

  • 새로 생성한 대부분의 객체는 Eden에 위치
  • Eden에서 GC가 한 번 발생한 후 살아남은 객체는 Survivor 영역 중 하나로 이동된다.
    • 이후 GC는 살아남은 객체가 존재하는 Survivor 영역으로 계속 쌓이게 된다.
  • 하나가 다 차면 그 중에서 살아남은 객체를 다른 survivor 영역으로 이동한다.
    • 가득 찬 Survivor 영역은 아무 데이터도 없는 상태가 된다.
  • 과정 반복 중 계속해서 살아남아 있는 객체는 Old 영역으로 이동한다.

→ 두 survivor 영역 중 하나는 반드시 비어있는 상태여야 한다.

  • 모두 데이터 존재 / 모두 사용량 0 → 비정상적인 시스템

빠른 메모리 할당을 위한 기술

  1. bump-the-pointer
  2. Thread-Local Allocation Buffers

Bump-the-pointer

Eden 영역에 할당된 마지막 객체 추적

  • Eden 영역의 맨 위(top)에 위치한다. → stack으로 관리
  • 새로운 객체를 생성할 때 마지막에 추가된 객체만 점검한다.
    • 크기가 Eden 영역에 넣기 적당한지 확인
    • 매우 빠른 메모리 할당 → 할당된 메모리의 바로 뒤에 메모리를 할당
  • 멀티스레드 환경
    • Thread-Safe를 위해 여러 스레드에서 사용하는 객체를 Eden 영역에 저장하려면 lock이 발생할 수밖에 없고, lock-contention 때문에 성능이 매우 떨어진다.

    • 메모리에 변경을 가하는 것 → Lock과 같은 동기화 작업을 수반함.

    • 여러 스레드가 가장 최근 할당된 object 뒤의 공간을 동시에 요청하면 동기화 이슈가 발생한다.

      → HotSpot VM에서 TLABs로 해결

TLAB(Therad-Local Allocation Buffers)

  • 각각의 스레드가 각각의 몫에 해당하는 Eden 영역의 작은 덩어리를 가질 수 있도록 하는 것.
  • 각 스레드는 자기가 갖고 있는 TLAB에만 접근 가능하다.
    • bump-the-pointer를 사용하더라도 아무런 lock이 없이 메모리 할당이 가능하다.


5개의 스레드가 동시에 할당 요청

  • 일반적인 경우 : T1이 먼저 heap lock을 걸고 할당 수행, 나머진 대기
  • TLAB : 스레드별로 heap 공간을 주어 대기 없이 allocation이 이루어지도록 함.
  • 단, TLAB을 최초로 할당하거나 부족해서 새로 할당을 받을 때는 동기화 이슈가 발생한다. 그래도 object allocation 횟수에 비한다면 시간이 상당히 줄어든다.

Old 영역에 대한 GC

기본적으로 데이터가 가득 차면 GC를 실행한다. GC 방식은 JDK 7을 기준으로 5가지 방식이 존재한다.

  • Serial GC
  • Parallel GC
  • Parallel Old GC(Paralel Compacting GC)
  • Concurrent Mark & Sweep GC(=CMS)
  • G1(Garbage First) GC

이 중 Serial GC는 CPU 코어가 하나만 있을 때 사용하기 위해서 만든 방식이므로 앱의 성능이 많이 떨어지기 때문에 운영 서버에서 절대 사용하면 안된다.

Serial GC(-XX:+UseSerialGC)

Old 영역의 GC는 mark-sweep-compact 라는 알고리즘을 사용한다.

  1. Old 영역에 살아 있는 객체를 식별한다(Mark)
  2. Heap의 앞 부분부터 확인하여 살아있는 것만 남긴다.(Sweep)
  3. 각 객체들이 연속되게 쌓이도록 힙의 가장 앞 부분부터 채워서 객체가 존재하는 부분과 없는 부분으로 나눈다.(Compaction)
  • 메모리와 CPU 코어 개수가 적을 때 적합한 방식

Parallel GC

Serial GC와 기본적인 알고리즘은 같다. (mark-sweep-compact)

  • 빠르게 객체를 처리할 수 있다.
  • 메모리가 충분하고 코어의 개수가 많을 때 유리하다.
  • Throughput GC라고도 부른다.
    • multiple cpu로 application throughput 향상

  • Serial은 스레드가 한 개, parallel은 스레드가 여러 개이다.
    • 커맨드 : java -XX:+UseParallelGC -jar demo.jar
    • -XX:ParallelGCThreads=N : GC 스레드 수 조절

Parallel Old GC(-XX:+UseParallelOldGC)

JDK 5 update 6부터 제공한 GC 방식.

  • parallel gc와 비교하면 old 영역의 GC 알고리즘만 다르다.
  • Mark-Summary-Compaction
    • Summary 단계는 앞서 GC를 수행한 영역에 대해서 별도로 살아 있는 객체를 식별한다.
      • Sweep보다 약간 더 복잡한 단계

CMS GC

  • Initial Mark : 클래스 로더에서 가장 가까운 객체 중 살아 있는 객체만 찾는 것으로 끝난다.
    • 멈추는 시간이 매우 짧다.
  • Concurrent Mark : 방금 살아있다고 확인한 객체에서 참조하고 있는 객체들을 따라가면서 확인한다.
    • 다른 스레드가 실행 중인 상태에서 동시에 진행된다는 것이 특징이다.
      • GC를 진행하는 다른 스레드를 의미하는 건지?
  • Remark : 이전 단계에서 새로 추가되거나 참조가 끊긴 객체를 확인한다.
  • Concurrent Sweep : 쓰레기를 정리하는 작업 실행
    • 다른 스레드가 실행되고 있는 상태에서 진행

→ Stop-the-world 시간이 매우 짧아 모든 애플리케이션의 응답 속도가 매우 중요할 때 사용하며, Low Latency GC라고도 부른다.

단점

  • 다른 GC방식보다 메모리와 CPU를 더 많이 사용한다.
  • Compaction 단계가 기본적으로 제공되지 않는다.
    • 조각난 메모리가 많아 compaction 작업을 실행하면 다른 gc보다 stop-the-world 시간이 더 길기 때문에 작업이 얼마나 오래, 자주 수행되는지 확인해야 한다.

G1 GC(-XX:+UseG1GC)

HotSpot Virtual Machine Garbage Collection Tuning Guide

바둑판의 각 영역에 객체를 할당하고 GC를 실행한다. 그러다가 해당 영역이 꽉 차면 다른 영역에서 객체를 할당하고 GC를 실행한다.

  • young → old로 이동하는 단계가 사라진 방식
    • young gen과 old gen으로 heap을 나누는 건 다른 컬렉터들과 비슷하다. 대신 Space-reclamation은 주로 young gen에서 이뤄지고, old gen에서는 가끔(ocassional) 수행된다고 한다.
  • CMS GC를 대체하기 위해 만들어짐 → 공식문서에선 기본 수집기라고 함
  • heap 사이즈 최대 수십gb 이상
  • fragmentation이 많음.
  • 예측 가능한 pause-time target goals < 몇백ms

  • jdk 6에선 early access로 시험사용만 가능하게했고, java 9부터 default collector가 되었다.
  • 실시간(real-time) 컬렉터는 아니다.


🟥 : Eden 영역

🟥 with S : Survivor 영역

🟦 : Old 영역

🟦 with H : humongous 오브젝트

  • 여러 old 영역을 한 번에 차지한다.
  • 대부분 새로 생성되는 객체는 eden regions에 할당된다.
  • humongous 오브젝트는 처음부터 old gen에 직접적으로 할당된다.

Garbage Collection Cycle


G1은 stop-the-world 동안 GC와 space reclamation을 수행한다.

두 가지 단계가 번갈아 일어난다.

Young-only phase : 일반적인 young collections로 시작하고, 두 phase 사이 전환은 old gen의 점유율이 특정한 임계값을 넘어가면 일어난다.

  • 현재 사용 가능한 메모리를 old gen의 객체로 점차 채워나가는 단계이다.
  • 객체를 old gen으로 승격시키는 몇 개의 Normal Young Collection으로 시작한다.
    • young gen 중에 old gen 영역으로 이동시키는 일반적인 collection을 의미하는 것 같다.
  • 두 phase 간의 전환은 old gen의 점유율이 Initiating Heap Occupancy 임계값에 도달하면 시작 된다.
    • Normal young 대신 Concurrent start young collection을 스케줄링한다.
  • Concurrent Start : Normal young collection 외에도 marking 프로세스를 수행한다. old gen 영역에서 다음 단계(space-reclamation)까지 유지할 현재 살아있는 모든 객체들을 마킹한다.
    • collection marking이 완전히 끝나지 않은 동안에도, normal young collection이 이뤄질 수 있다.
    • Remark, cleanup(stop-the-world pauses임)으로 끝난다.
  • Remark : Marking 자체를 마무리하며, 전역 참조 처리 및 class unloading, 빈 영역 회수, 내부 데이터 구조 정리 등을 수행한다.
  • Remark와 clean up 단계 사이에서 G1은 나중에 선택된 old gen 영역에서 동시에 회수 가능한 free space를 계산한다. 이 계산은 Cleanup pause에서 마무리된다.
    • Adaptive IHOP : 이전 GC의 동작과 Heap 영역의 상태를 고려해서 threshold를 수정하며 stop-the-world와 처리율간의 밸런스를 점진적으로 맞춰나간다.
  • Cleanup : space-reclamation 단계가 이어질 지를 결정한다.
    • 다음 단계 진행 시, young-only phase는 Single Prepare Mixed Young Collection으로 전환된다.

Space Reclamation : young과 old 영역을 구분짓지 않고, 힙 영역에 저장되어 있는 객체들 상대로도 메모리를 회수해가는 과정

  • Multiple Mixed Collections로 구성된다.
    • Young gen 영역 + old gen 영역의 라이브 객체 셋을 비운다.
  • G1이 old gen 영역을 비우는 게 노력할 만큼 충분한 여유 공간을 확보하지 못한다고 판단하면 이 단계가 종료된다.

이후 사이클을 또 다른 young-only phase로 다시 시작된다.

liveness 정보를 수집하는 동안 메모리가 부족해지면(out-of-memory) 다른 gc처럼 제자리에서 stop-the-world 전체 heap compaction(Full GC)을 수행한다.

  • liveness 정보?

Garbage Collection Pauses and Collection Set

G1은 GC와 공간 회수를 stop-the-world pauses 중에 수행한다.

  • 라이브 객체들은 일반적으로 source regions에서 하나 이상의 destination regions로 복사된다.
  • 이동되는 객체에 존재하는 참조는 조정된다.
  1. non-humongous regions : destination은 해당 객체의 source에 따라 결정된다.
    1. Young generation(eden/survivor) → age에 따라 survivor나 old gen으로 복사된다.
    2. old regions → old regions
  2. Humongous regions : G1은 humongous 객체들을 절대 이동시키지 않고, liveness만 확인해서 죽었으면 공간을 회수한다.

Collection Set은 회수할 공간의 source regions의 set이다.

phase에 따라 collection set은 다른 종류의 regions로 구성된다.

  • Young-Only : young gen, humongous regions(회수될 가능성이 있는 객체들로 이루어진)
  • Space-Reclamation : young-only와 동일한데, collection set candidate regions의 몇몇 old gen regions가 추가된다.

G1은 Remark pause 동안 점유율이 낮은, free space가 많은 영역을 선택한다. 이러한 영역들은 remark와 Cleanup 사이에 이후 collection을 위해 준비된다.

Cleanup pause는 이렇게 준비된 애들을 efficiency에 따라서 정렬한다.

  • collection 시간이 짧거나, 여유 공간이 많은 regions가 이후 mixed collections에서 선호된다.

G1 GC는 할당할 메모리 공간이 충분한지 판단해서 Full GC, Minor GC, Mixed GC 중 뭘 동작할지 결정한다.

  • Mixed GC는 영역을 구분짓지 않고 동작하는데, 전체를 스캔하는 게 아니라 일부만 수집해서 메모리를 회수하기 때문에 Full GC보다 시간이 덜 소모된다.
  • Adaptive IHOP은 Full GC가 발생하지 않도록 IHOP 값을 수정한다.
    • 활성화 : -XX:+G1UseAdaptiveIHOP
    • 비활성화 : -XX:-G1UseAdaptiveIHOP
      • 꺼두면, -XX:InitiatingHeapOccupancyPercent 값에 따라서 marking 작업이 트리거되는 때가 달라진다.

다음 → Java의 GC 상황을 모니터링 하는 방법과 GC 튜닝 방법

💡 각 서비스의 WAS에서 생성하는 객체의 크기와 생존 주기가 모두 다르고, 장비의 종류도 다양하기 때문에 WAS의 스레드 개수와 장비 당 WAS 인스턴스 개수, GC 옵션 등은 지속적인 튜닝과 모니터링을 통해서 해당 서비스에 가장 적합한 값을 찾아야 한다.
profile
SKT DEVOCEAN YOUNG 2기, Kubernetes Korea Group

0개의 댓글