개발/Java

GC (Garbage Collection)

Debin 2023. 12. 11.
반응형

오늘은 GC에 대해 공부를 진행해보겠습니다.

 

Stop The World

stop-the-world는 GC를 실행하기 위해 JVM이 애플리케이션 실행을 멈추는 것이다.

stop-the-world가 발생하면 GC를 실행하는 스레드를 제외한 나머지 스레드는 모두 작업을 멈춘다.

GC 작업을 완료하 이후에야 중단했던 작업을 다시 시작한다.

어떤 GC 알고리즘을 사용하더라도 stop-the-world는 발생한다.

 

대개의 GC 튜닝은 stop-the-world 시간을 줄이는 것이다.

 

GC

자바에서는 개발자가 프로그램 코드로 메모리를 명시적으로 해제하지 않기 때문에 Garbage Collector가 더 이상 필요 없는 객체를 찾아 지우는 작업을 한다.

 

필요 없는 객체란 유효한 참조가 없어진 객체다.

즉 JNI 스택에 의한 참조, 자바 스택에 의한 참조, 정적 변수에 의한 참조 사슬과 무관한 객체라면 GC 대상이 된다.

 

Garbage Collector는 두 가지 가설 하에 만들어졌다.

 

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

이 가설의 장점을 최대한 살리기 위해 HotSpot VM에서는 크게 2개로 물리적 공간을 나누었다.

둘로 나눈 공간이 Young 영역과 Old 영역이다.

 

HotSpot VM이란?

HotSpot VM은 인터프리티드 모드로 실행하는 동안 애플리케이션을 모니터링하면서 가장 자주 실행하는 코드 파트를 발견해 JIT 컴파일을 수행한다. 

 

JIT 컴파일이란?

자바에서 바이트 코드를 실행하는게 인터프리터와 JIT 컴파일러다.

자바를 실행할 때 인터프리터는 바이트 코드를 한 줄씩 실행한다. 이 인터프리터 효율을 높이기 위해, JIT 컴파일을 사용한다.

실행 시점에서 인터프리터 방식으로 네이티브(특정 OS의 기계어) 코드를 생성한다.

 

만약 같은 코드(함수 등)가  발견되면 인터프리터는 JIT 컴파일러를 통해 반복되는 코드를 모두 네이티브 코드로 바꿔둔다. (캐시)

그 이후에는 인터프리터가 반복되는 코드를 발견한다면 JIT 컴파일러를 통해 네이티브 코드로 컴파일된 코드를 바로 사용한다.

 

JIT(Just-In-Time) 컴파일러는 런타임에 자바 애플리케이션의 성능을 향상시키는 자바 런타임 환경의 구성 요소다.

 

Young 영역

새롭게 생성할 객체의 대부분이 여기에 위치한다.

대부분의 객체가 금방 접근 불가능한 상태가 되기 때문에 많은 객체가 Young 영역에 생성되었다가 사라진다.

이 영역에서 객체가 사라질 때 Minor GC가 발생한다.

 

Old 영역

접근 불가능한 상태로 되지 않아 Young 영역에서 살아남은 객체가 여기로 복사된다.

대부분 Young 영역보다 크게 할당하며, 크기가 큰 만큼 Young 영역보다 GC는 적게 발생한다.

이 영역에서 객체가 사라질 때 Major GC(Full GC)가 발생한다고 말한다.

 

 

OOM 현상은  Young영역, Old 영역, Metaspace 영역 혹은 Native영역에서 모두 발생할수 있다. 각 영역별로 메모리릭이 나는 원인이 다르기에, 메모리릭을 찾는 일은 그리 쉽지 않은 작업일 수 있다. 

 

영역별 데이터 흐름을 그림으로 살펴보면 다음과 같다.

 

GC 영역 및 데이터 흐름도

 

Permanent Generation은 Mehtod Area라고도 한다. (이 영역은 자바 8 버전에서 Metaspace로 바뀌었다)

객체나 억류된 문자열 정보를 저장하는 곳이며, Old 영역에서 살아남은 객체가 영원히 남아있는 곳은 아니다.

이 영역에서 GC가 발생할 수도 있는데, 여기서 GC가 발생해도 Major GC의 횟수에 포함된다.

 

 그럼 Old 영역에 있는 객체가 Young 영역의 객체를 참조하는 경우가 있을 때에는 어떻게 처리될까?

이러한 경우를 처리하기 위해서 Old 영역에는 512바이트의 덩어리(chunk)로 되어 있는 카드 테이블(card table)이 존재한다.

 

카드 테이블에는 Old 영역에 있는 객체가 Young 영역의 객체를 참조할 때마다 정보가 표시된다.

Young 영역의 GC를 실행할 때에는 Old 영역에 있는 모든 객체의 참조를 확인하지 않고, 이 카드 테이블만 뒤져서 GC 대상인지 식별한다.

 

카드 테이블 구조

 

카드 테이블은 write barrier를 사용하여 관리한다. write barrier는 Minor GC를 빠르게 할 수 있도록 하는 장치이다.

write barrirer때문에 약간의 오버헤드는 발생하지만 전반적인 GC 시간은 줄어들게 된다.

 

Young 영역의 구성

Young 영역은 3개의 영역으로 나뉜다.

 

  • Eden 영역
  • Survivor 영역(2개)

Survivor 영역이 2개이기 때문에 총 3개의 영역으로 나뉘는 것이다.

각 영역의 처리 절차를 순서에 따라 정리하면 다음과 같다.

 

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

Survivor 영역 중 하나는 반드시 비어있는 상태로 남아있어야 한다.

만약 두 Survivor 영역에 데이터가 존재허간, 두 영역 모두 사용량이 0이라면 시스템이 정상적인 상황이 아닌 것이다.

 

GC전과 후 비교

 

참고로 HotSpot VM에서는 보다 빠른 메모리 할당을 위해 두 가지 기술을 사용한다.

하나는 bump-the-pointer라는 기술이며, 다른 하나는 TLABs(Thread-Local Allocation Buffers)라는 기술이다.

 

bump-the-pointer는 Eden 영역에 할당된 마지막 객체를 추적한다.

마지막 객체는 Eden 영역의 맨 위(top)에 있다. 

그리고 그 다음에 생성되는 객체가 있으면, 해당 객체의 크기가 Eden 영역에 넣기 적당한지만 확인한다.

만약 해당 객체의 크기가 적당하다고 판정되면 Eden 영역에 넣게 되고, 새로 생성된  객체가 맨 위에 있게 된다.

따라서 새로운 객체를 생성할 때 마지막에 추가된 객체만 점검하면 되므로 매우 빠르게 메모리 할당이 이루어진다.

 

그러나 멀티 스레드 환경을 고려하면 이야기가 달라진다.

Thread-Safe하기 위해서 만약 여러 스레드에서 사용하는 객체를 Eden 영역에 저장하려면 락(lock)이 발생할 수 밖에 없고, lock-contention 때문에 성능은 매우 떨어지게 될 것이다. HotSpot VM에서 이를 해결한 것이 TLABs이다.

 

각각의 스레드가 각각의 몫에 해당하는 Eden 영역의 작은 덩어리를 가질 수 있도록 하는 것이다.

각 쓰레드에는 자기가 갖고 있는 TLAB에만 접근할 수 있기 때문에, bump-the-pointer라는 기술을 사용하더라도 아무런 락이 없이 메모리 할당이 가능하다.

 

Old 영역에 대한 GC

Old 영역은 기본적으로 데이터가 가득 차면 GC을 수행한다.

 

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

알고리즘의 첫 단계는 Old 영역에 살아 있는 객체를 식별(Mark)하는 것이다.

그 다음에는 힙(heap)의 앞 부분부터 확인하여 살아 있는 것만 남긴다(Sweep).

마지막 단계에서는 각 객체들이 연속되게 쌓이도록 힙의 가장 앞 부분부터 채워서 객체가 존재하는 부분과 객체가 없는 부분으로 나눈다(Compaction).

 

 

GC 방식에 따라서 처리 절차가 달라지므로, 어떤 GC 방식이 있는지 살펴보자.

 

Serial GC

  • 단일 스레드 환경 및 소규모 응용 프로그램을 위한 간단한 GC
  • Minor GC에서 Copy & Scavenge 알고리즘 적용
  • Full GC에서 Mark & Compact 알고리즘 적용
  • 실무에서는 사용 X

 

Parallel GC

  • JVM 기본 옵션(Java 8)
  • 멀티 스레드 기반으로 작동해 효율을 높인다.
  • 메모리가 충분하고 코어가 많을 때 유리하다.
  • Low-pause (응용 프로그램 중단 최소화)
  • Throughput GC라고도 부른다.

 

CMS GC

  • Java 14에서 G1 GC를 지원하고자 완전히 제거되었다.

 

G1 GC

 

G1(Garbage First) GC에 대해서 알아보자. G1 GC를 이해하려면 지금까지의 Young 영역과 Old 영역에 대해서는 잊는 것이 좋다.

 

G1 GC 구조

 

G1 GC는 바둑판의 각 영역에 객체를 할당하고 GC를 실행한다.

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

즉, 지금까지 설명한 Young의 세가지 영역에서 데이터가 Old 영역으로 이동하는 단계가 사라진 GC 방식이라고 이해하면 된다.

G1 GC는 과거CMS GC를 대체하기 위해서 만들어 졌다.

 

G1 GC의 가장 큰 장점은 성능이다.

4GB 이상 대용량 Heap 메모리를 사용하는 멀티 스레드 기반 응용 프로그램에 특화된 GC다.

Heap 영역을 미리 (1 ~ 32MB) 단위로 분할한 후 멀티스레드로 스캔한다.

가비지가 가장 영역부터 수집을 실시한다.

 

참고자료

https://d2.naver.com/helloworld/1329

https://www.inflearn.com/course/%EB%A9%B4%EC%A0%91-%EC%8B%A0%EC%9E%85-java-%EB%B0%B1%EC%95%A4%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90/dashboard

반응형

댓글