개발/Spring

가비지 컬렉터

728x90

가비지 컬렉션(Garbage Collection, 이하 GC)에 대해 잘 알고 있을수록 실력이 좋은 Java 개발자라고 생각합니다. GC 과정에 관심을 가질 정도라면 규모가 일정 이상인 애플리케이션을 제작해 본 경험이 있을 것입니다. 또, 어떤 GC 알고리즘을 선택할 것인지 고민할 정도면 스스로 제작한 애플리케이션의 특징을 정확히 이해하고 있다고 볼 수 있습니다. 이러한 판단 기준이 보편적이지는 않지만, GC에 대한 이해는 훌륭한 Java 개발자가 되기 위한 필수 조건이라는 데에는 별다른 이견이 없을 것입니다.

 

가비지 컬렉션 과정

GC에 대해서 알아보기 전에 알아야 할 용어가 있다. Stop-The-World이다. GC를 실행하기 위해 JVM이 애플리케이션 실행을 멈추는 것이다.

GC를 실행하는 쓰레드를 제외한 나머지 쓰레드는 모두 작업을 멈춘다. GC작업을 완료한 이후에야 중단했던 작업을 다시시작한다. 어떤 GC알고리즘을 사용하더라도 STW는 발생한다. 대개의 경우 GC튜닝이란, 이 STW시간을 줄이는 것이다.

 

 

자바에서 소스상에서 메모리를 명시적으로 지정하여 해제하지 않는다.

가끔 명시적으로 해제하려고 해당 객체를 null로 지정하거나 System.gc() 메서드를 호출하는 개발자가 있다. null로 지정하는 것은 문제가 되지 않지만, System.gc()메서드를 호출하는 것은 시스템 성능에 매우 큰 영향을 끼치므로 절대 호출해서는 안된다.

 

Java에서는 개발자가 프로그램 코드로 메모리를 명시적으로 해제하지 않기 떄문에 가비지 컬렉터가 더이상 필요없는 쓰레기 객체를 찾아 지우는 작업을 한다. 이 가비지 컬렉터는 두 가지 조건 하에 만들어 졌다.

 

  • 대부분 객체는 접근 불가 상태가 된다
  • 오래된 객체에서 젊은 객체로의 참조는 아주 적다

이러한 가설을 'weak generational hypothesis'라 한다. 이 가설의 장점을 최대한 살리기 위해 HotSpot VM에서는 크게 2개로 물리적 공간을 나누었다. 둘로 나눈 공간이 Young, Old이다.

 

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

GC영역 및 데이터 흐름도

Permanent Generation 영역은 Method Area라고도 한다. 객체나 역류된 문자열 정보를 저장하는 곳이며, Old 영역에서 살아남은 객체가 영원히 남아 있는 곳은 절대 아니다. 이 영역에서 GC가 발생할 수도 있는데, 여기서 GC가 발생해도 Major GC의 횟수에 포함된다.

 

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

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

 

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

 

카드 테이블 구조

카드 테이블은 write barrier를 사용하여 관리한다. write barrier는 Minor GC를 빠르게 할수 있도록 하는 장치이다. write barrirer때문에 약간의 오버헤드는 발생하지만 전반적인 GC시간은 줄어들게 된다.

(오버헤드 : 어떤 처리를 하기 위해  들어가는 간접적인 처리 비용*시간 메모리 등을 말한다.)

 

 

Young 영역의 구성

GC를 이해하기 위해서 객체가 제일 먼저 생성되는 Young 영역부터 알아보자. Young 영역은 3개의 영역으로 나뉜다.

 

  • Eden
  • Survivor(2개)

 

Survivor영역이 두개이기 때문에 총 3개의 영역으로 나뉜다. 

 

각 영역의 처리 순서는 아래와 같다

 

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

 

이 절차를 확인해보면 알겠지만 Survivor 영역 중 하나는 반드시 비어 있는 상태로 남아 있어야 한다. 만약 두 Survivor 영역에 모두 데이터가 존재하거나, 두 영역 모두 사용량이 0이라면 여러분의 시스템은 정상적인 상황이 아니라고 생각하면 된다.

 

 

이렇게 Minor GC를 통해서 Old 영역까지 데이터가 쌓인 것을 모식화 하면 아래와 같다.

 

Minor GC 전과 후의 비교

참고로 HotSpot VM에서는 보다 빠른 메모리 할당을 위해서 두 가지 기술을 사용한다. 하나는 bump-the-pointer라는 기술이며, 다른 하나는 TLABs(Thread-Local Allocation Buffers)라는 기술이다.

 

bump-the-pointer는 Eden 영역에 할당된 마지막 객체를 추적한다. 마지막 객체는 Eden영역의 맨 위에 있다. 그리고 그 다음에 생성되는 객체가 있으면, 해당 객체의 크기가 Eden영역에 넣기 적당한지만 확인한다. 만약 해당 크기가 적당하면 영역에 넣게 되고 , 새로 생성된 객체가 맨 위에 있게 된다. 새로운 객체를 생성할 때 마지막에 추가된 객체만 점검하면 되므로 매우 빠르게 할당이 이뤄진다.

 

그러나 멀티 스레드 환경을 고려하면 이야기가 달라진다. Thread-safe하기 위해서 만약 여러 스레드에서 사용하는 객체를 Eden영역에 저장하면 락(lock)이 발생할 수 밖에 없고, lock-contention 떄문에 성능은 매우 떨어지게 될 것이다. HotSpot VM에서 이를 해결한 것이 TLABs이다.

 

(Thread-safe : 멀티 스레드 프로그래밍에서 일반적으로 어떤 함수나 변수, 혹은 객체가 여러 스레드로부터 동시 적근이 이뤄져도 프로그램의 실행 문제가 없음을 뜻한다. 엄밀하게는 하나의 함수가 한 스레드로부터 호출되어 실행중일 때, 다른 스레드가 함수를 호출하여 동시에 함께 실행되더라도 각 스레드에서의 함수의 수행 결과가 올바로 나오는 것으로 정의한다.)

 

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

 

간단하게 Young영역에 대한 GC에 대해서 알아보았다. 위에서 이야기한 두 가지 기술(bump-the-pointer, TLABs)을 반드시 기억하고 있을 필요는 없다. 

 

 

Eden 영역에 최초로 객체가 만들어지고 Survivor영역을 통해서 Old 영역으로 오래 살아남은 객체가 이동한다는 사실은 꼭 기억하기 바란다.

 

 

 

 

Old 영역에 대한 GC

 

Old 영역은 기본적으로 데이터가 가득 차면 GC를 실행한다. GC 방식에 따라서 처리 절차가 달라지므로, 어떤 GC 방식이 있는지 살펴보자

JDK 7 기준으로 5가지 방식이 있다.

 

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

이중에서 운영서버에서 절대 사용하면 안 되는 방식이 Serial GC다. Serial GC는 데스크톱의 CPU 코어가 하나만 있을 때, 사용하기 위해서 만든 방식이다. Serial GC를 사용하면 애플리케이션의 성능이 많이 떨어진다.

 

Serial GC (-XX : + UseSerialGC)

 

Young 영역에서의 GC는 앞 절에서 설명한 방식을 사용한다. Old 영역의 GC는 mark-sweep-compact이라는 알고리즘을 사용한다 

이 알고리즘의 첫 단계는 Old영역에 살아 있는 객체를 식별하는 것이다. 그 다음에는 힙의 앞 부분부터 확인하여 살아 있는 것만 남긴다. 마지막 단계에서는 각 객체들이 연속되게 쌓이도록 힙의 가장 앞 부분부터 채워져 객체가 존재하는 부분과 객체가 없는 부분으로 나눈다. (Compaction)

 

적은 메모리와 CPU코어 갯수가 적을 때 적합한 방식이다.

 

Parallel GC (-XX:+UseParallelGC)

 

Parallel GC는 Serial GC와 기본적인 알고리즘은 같지만, Serial GC는 GC를 처리하는 스레드가 하나인 것에 비해, Parallel GC는 GC를 처리하는 쓰레드가 여러개다. 그렇기 때문에 Serial GC보다 빠르게 객체를 처리할 수 있다.

Parallel GC는 메모리가 충분하고 코어 갯수가 많을 때 유리하다. Parallel GC는 Throughput GC라고도 부른다.

Serial GC, Parallel GC의 차이

 

Parallel Old GC (-XX : +UseParallelOldGC)

 

Parallel Old GC는 JDK 5 update 6부터 제공한 GC 방식이다. 앞서 설명한 Parallel GC와 비교하여 Old 영역의 GC알고리즘만 다르다. 이 방식은 Mark-Summary-Compaction 단계를 거친다. Summary 단계는 앞서 GC를 수행한 영역에 대해서 별도로 살아있는 객체를 식별한다는 점에서 Mark-Sweep-Compaction알고리즘의 Sweep단계와 다르며, 약간 더 복잡한 단계를 거친다. 

 

CMS GC (-XX +UseConcMarkSweepGC)

 

다음 그림은 Serial GC와 CMS GC의 절차를 비교한 그림이다.

Serial GC, CMS GC

초기 Initial Mark단계에서는 클래스 로더에서 가장 가까운 객체 중 살아 있는 객체만 찾는 것으로 끝낸다. 따라서 멈추는 시간은 매우 짧다. 그리고 Concurrent Mark단계에서는 방금 살아있다고 확인한 객체에서 참조하고 있는 객체들을 따라가면서 확인한다  이 단계의 특징은 다른 스레드가 실행중인 상태에서 동시에 진행된다는 것이다.

 

그 다음 Remark 단계에서는 Concurrent Mark 단계에서 새로 추가되거나 참조가 끊긴 객체를 확인한다. 마지막으로 Concurrent Sweep단계에서는 쓰레기를 정리하는 작업을 실행한다. 이 작업도 다른 스레드가 실해오디고 있는 상황에서 진행된다.

 

이러한 단계로 진행되는 GC방식이기 때문에 STW시간이 매우 짧다. 모든 애플리케이션의 응답 속도가 매우 중요할 때 CMS GC를 사용하며, LOW Latency GC라고도 부른다.

 

하지만 STW시간이 짧은 대신에 아래와 같은 단점이 존재한다.

 

  • 다른 GC 방식보다 메모리와 CPU를 더 많이 사용한다.
  • Compaction단계가 기본적으로 제공되지 않는다

따라서 CMS GC를 사용할 때에는 신중히 검토한 후에 사용해야 한다. 그리고 조각난 메모리가 많아 Compaction 작업을 실행하면 다른 GC방식의 STW시간보다 STW시간이 더 길기 때문에 Compaction작업이 얼마나 자주 오랫동안 수행되는지 확인해야 한다.

 

 

 

G1 GC

 

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

 

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

장기적으로 말도 많고 탈도 많은 CMS GC를 대체하기 위해서 만들어 졌다.

 

 

 

 

어떤 객체를 생성하게 되면 그 객체는 언젠가는 쓰레기가 되어 메모리에서 지워져야만 한다.

C에서는 해체하는 작업을 일일이 해줘야 하지만 자바는 GC가 알아서 쓰레기 메모리들을 치워준다.

 

Java 7부터 공식적으로 사용할 수 있는 Garbage First (G1) 이라는 가비지 컬렉터를 제외한 나머지 JVM은 다음과 같이 영역을 나누어 힙이라는 공간에 객체들을 관리한다.

 

 

가장 왼쪽에 있는 Young 영역은 말 그대로 젊은 객체들이 존재하며, Old 영역에는 늙은 객체들이 자리잡게 된다.

그리고 Perm이라는 영역에는 클래스나 메소드에 대한 정보가 쌓인다.

(Perm에 저장되는 데이터는 더 많지만, 이 정도만 알고 있자)

Young 영역은 Eden과 두 개의 Surviror영역으로 나뉘는데, 이 중에서는 우리가 객체를 생성하자마자 저장되는 저장소는 Eden이다. 일반적으로 자바에서 메모리가 살아가는 과정은 아래와 같다.

 

1) Eden 영역에서 객체가 생성된다.

2) Eden 영역이 꽉 차면 살아있는 객체만 Survivor 영역으로 복사되고, 다시 Eden 영역을 채우게 된다. 

3) Survivor 영역도 꽉 차게 되면 다른 Survivor 영역으로 객체가 복사된. 이때 Eden 영역에 있는 객체들 중 살아있는 객체들도 다른 Surviror 영역으로 간다. 즉 Survivor 영역의 둘 중 하나는 반드시 비어있어야 한다./

 

이 과정은 마이너 혹은 영 가비지 컬렉션이라고 한다.

 

그러나 오래 살아있는 객체들은 Old 영역으로 이동한다. 지속적으로 이동하다가 Old이 꽉차면 메이져, Full GC가 발생한다.

 

 

영GC가 풀GC보다 빠르다.왜냐하면 일반적으로 더 작은 공간이 할당되고, 객체들은 처리하는 방식도 다르기 때문이다. 그렇다고 전체의 힙 영역을 영 영역으로 만들면 장애로 이뤄질 확률이 높아진다.

 

오라클 JDK에서 제공하는 GC의 방식은 크게 4가지가 있고, G1을 포함하면 5가지의 가비지 컬렉터가 존재한다.

 

  • Serial GC
  • Parallel Young Generation Collector
  • Parallel Old Generation Collector
  • Concurrent Mark & Sweep Collector (CMS)
  • G1

여기서 WAS에서 Serial GC를 사용하면 안된다 . 클라이언트용 장비에 최적화 되어 있기 때문에, GC 속도가 매우 느려 웹앱이 매우 느려진다.

 

 

애플리케이션에서 만들어지는 모든 객체의 크기와 종류가 같다면 회사에서 사용하는 모든 WAS와 GC옵션을 동일하게 설정할 수 있다. 하지만 각 서비스의 WAS에서 생성하는 객체의 크기와 생존 주기가 모두 다르고 장비의 종류가 다양하다. WAS의 스레드 객수와 장비당 WAS 인스턴스 갯수, GC옵션 등은 지속적인 튜닝과 모니터링을 통해서 해당 서비스에 가장 적합한 값을 찾아야 한다.

 

 

'개발 > Spring' 카테고리의 다른 글

자바 Java 제네릭 Generic  (0) 2021.03.05
자바 I/O  (0) 2021.03.05
java.lang 패키지  (0) 2021.02.28
JVM이란?  (0) 2021.02.28
Hot Spot JVM이란?  (0) 2021.02.27