본문 바로가기
Programming language/Java

[Java] GC란?

by hyeinisfree 2022. 5. 12.

GC(Carbage Collection)란?

가비지 컬렉션(Garbage Collection)

가비지 컬렉션(Garbage Collection)은 프로그램이 동적으로 할당했던 메모리 영역 중에서 필요 없게 된 영역을 탐지하여 자동으로 해제하는 기능이다. 가비지 컬렉션은 자바만의 기능이 아니다. 개발자가 힙을 사용할 수 있는 만큼 자유롭게 사용하고, 더 이상 사용되지 않는 객체들은 가비지 컬렉션을 담당하는 프로세스가 자동으로 메모리에서 제거하도록 하는 것이 가비지 컬렉션의 기본 개념이다.

가비지 컬렉터(Garbage Collector)

가비지 컬렉터(Garbage Collector)는 가비지 컬렉션을 수행하는 담당자이다. JVM에는 가비지 컬렉션 알고리즘을 수행하는 가비지 컬렉터가 있다. 힙 메모리에 객체가 더 이상 필요없게 되면 JVM의 가비지 컬렉터가 메모리를 해제한다.

 

즉, 가비지 컬렉션이 동적 할당된 메모리 중 더 이상 사용하지 않는 영역을 탐지하여 자동으로 해제하는 기능 자체를 나타내는 용어라면, 가비지 컬렉터는 가비지 컬렉션 알고리즘을 실행하는 프로세스와 같이 실제 가비지 컬렉션의 수행을 담당하는 주체를 의미한다.

 

GC(Garbage Collection)의 장단점

장점

GC를 지원하는 환경에서는 프로그래머가 동적으로 할당한 메모리 영역의 전체를 완벽하게 관리할 필요가 없어진다. GC를 사용하면 다음과 같은 버그를 줄이거나 완전히 막을 수 있다.

  • 유효하지 않은 포인터 접근: 이미 해제된 메모리에 접근하는 버그를 가리킨다. 만약 이 포인터가 해제되고 새로운 값이 할당되었다면, 잘못된 값을 읽어오게 된다.
  • 이중 해제: 이미 해제된 메모리를 또다시 해제하는 버그를 가리킨다. 일부 메모리 할당 알고리즘에서는, 해제된 메모리를 다시 해제하려고 시도하는 것은 오류를 일으킬 수 있다.
  • 메모리 누수: 더이상 필요하지 않은 메모리가 해제되지 않고 남아있는 버그를 가리킨다. 메모리 누수가 반복되면 메모리 고갈로 프로그램이 중단될 수 있다. (접근 가능한 메모리가 증가하여 메모리가 고갈되는 문제는 쓰레기 수집으로도 막을 수 없다)

단점

  • 어떤 메모리를 해제할지 결정하는 데 비용이 든다. 객체가 필요없어지는 시점을 프로그래머가 미리 알고 있는 경우에도 쓰레기 수집 알고리즘이 메모리 해제 시점을 추적해야 하므로, 이 작업은 오버헤드가 된다.
  • 쓰레기 수집이 일어나는 타이밍이나 점유 시간을 미리 예측하기 어렵다. 때문에 프로그램이 예측 불가능하게 일시적으로 정지할 수 있다. 이런 특성은 특히 실시간 시스템에는 적합하지 않다.
  • 할당된 메모리가 해제되는 시점을 알 수 없다.

 

JVM Heap 구조

자바에서 new 연산자 등으로 생성된 객체(인스턴스)와 배열 등은 JVM의 Heap 영역에 저장된다. Heap 영역에 저장된 객체(인스턴스)나 배열은 다른 객체에 의해 참조 될 수 있다. 따라서 이 영역에서 GC가 발생하며, 참조가 없는 객체들을 메모리에서 제거하게 된다.

Weak Generational Hypothesis

자바는 처음부터 GC를 염두에 두고 설계되어, 언어 정의에 GC가 포함되어 있다. 자바의 GC는 아래의 두가지 가설을 전재로 도입되었다.

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

이를 Weak Generational Hypothesis라고 한다.

 

Hotspot Heap 구조

위 가설의 장점을 최대한 살리기 위해서 HotSpot VM에서는 크게 2개로 JVM의 Heap 영역의 물리적 공간을 나누었다. 둘로 나눈 공간이 아래 그림의 Young 영역과 Old 영역이다.

  • Young 영역(Yong Generation 영역): 새롭게 생성한 객체의 대부분이 여기에 위치한다. 대부분의 객체가 금방 접근 불가능 상태가 되기 때문에 매우 많은 객체가 Young 영역에 생성되었다가 사라진다. 이 영역에서 객체가 사라질때 Minor GC가 발생한다고 말한다.
  • Old 영역(Old Generation 영역): 접근 불가능 상태로 되지 않아 Young 영역에서 살아남은 객체가 여기로 복사된다. 대부분 Young 영역보다 크게 할당하며, 크기가 큰 만큼 Young 영역보다 GC는 적게 발생한다. 이 영역에서 객체가 사라질 때 Major GC(혹은 Full GC)가 발생한다고 말한다.
💡 Permanent 영역
Method Area라고도 한다. JVM이 클래스들과 메소드들을 설명하기 위해 필요한 메타데이터들을 포함하고 있다. JDK 8부터는 Metaspace로 교체되었다.

 

GC(Garbage Collection)의 동작 방식

Young 영역과 Old 영역은 서로 다른 메모리 구조로 되어 있기 때문에, 세부적인 동작 방식은 다르다. 하지만 기본적으로 가비지 컬렉션이 실행된다고 하면 다음의 2가지 공통적인 단계를 따르게 된다.

1. Stop The World

Stop The World는 가비지 컬렉션을 실행하기 위해 JVM이 애플리케이션의 실행을 멈추는 작업이다. GC가 실행될 때는 GC를 실행하는 쓰레드를 제외한 모든 쓰레드들의 작업이 중단되고, GC가 완료되면 작업이 재개된다. 당연히 모든 쓰레드들의 작업이 중단되면 애플리케이션이 멈추기 때문에, GC의 성능 개선을 위해 튜닝을 한다고 하면 보통 stop-the-world의 시간을 줄이는 작업을 하는 것이다. 또한 JVM에서도 이러한 문제를 해결하기 위해 다양한 실행 옵션을 제공하고 있다.

2. Mark and Sweep

  • Mark: 사용되는 메모리와 사용되지 않는 메모리를 식별하는 작업
  • Sweep: Mark 단계에서 사용되지 않음으로 식별된 메모리를 해제하는 작업

Stop The World를 통해 모든 작업을 중단시키면, GC는 스택의 모든 변수 또는 Reachable 객체를 스캔하면서 각각이 어떤 객체를 참고하고 있는지를 탐색하게 된다. 그리고 사용되고 있는 메모리를 식별하는데, 이러한 과정을 Mark라고 한다. 이후에 Mark가 되지 않은 객체들을 메모리에서 제거하는데, 이러한 과정을 Sweep라고 한다.

💡 Java GC는 객체가 가비지(Garbage)인지 판별하기 위해서 reachability라는 개념을 사용한다. 어떤 객체에 유효한 참조가 있으면 'reachable'로, 없으면 'unreachable'로 구별하고, unreachable 객체를 가비지로 간주해 GC를 수행한다. 이러한 과정을 수행하는 알가비지 선정 알고리즘(가비지 수집 알고리즘) 중 하나가 Mark and Sweep인 것이다. 다른 방식으로는 Reference Counting과 같은 방식이 있다. 자바 객체들의 참조와 Mark and Sweep에 대해 더 궁금하다면, Java Reference와 GC 글을 참고하자.

 

Young 영역에서의 GC(Minor GC)

Young 영역은 또 다시 3개의 영역으로 나누어져 있다.

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

Survivor 영역이 2개이기 때문에 총 3개의 영역으로 나뉘는 것이다. 각 영역의 처리 절차를 순서에 따라서 기술하면 다음과 같다.

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

이 절차를 확인해 보면 알겠지만 Survivor 영역 중 하나는 반드시 비어 있는 상태로 남아 있어야 한다. 위에서 말하는 GC는 위에서 말하는 기본적인 GC의 동작 과정을 따라 진행된다고 이해하면 된다.

 

이렇게 Minor GC를 통해서 Old 영역까지 데이터가 쌓인 것을 그림으로 간단히 나타내면 다음과 같다.

 

Old 영역에서의 GC(Major GC)

Young 영역에서 오래 살아남은 객체는 Old 영역으로 Promotion 된다. 이러한 과정을 통해 Old 영역의 데이터가 가득 차면 Major GC를 실행한다.

💡 Young 영역은 일반적으로 Old 영역보다 크키가 작기 때문에 GC가 보통 0.5초에서 1초 사이에 끝난다. 그렇기 때문에 Minor GC는 애플리케이션에 크게 영향을 주지 않는다. 하지만 Old 영역은 Young 영역보다 크며 Young 영역을 참조할 수도 있다. 그렇기 때문에 Major GC는 일반적으로 Minor GC보다 시간이 오래걸리며, 10배 이상의 시간을 사용한다.

 

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)

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

 

Serial GC는 적은 메모리와 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의 절차를 비교한 그림이다. 그림에서 보듯이 CMS GC는 지금까지 설명한 GC 방식보다 더 복잡하다.

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

 

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

 

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

 

그런데 CMS GC는 stop-the-world 시간이 짧다는 장점에 반해 다음과 같은 단점이 존재한다.

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

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

 

G1 GC

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

 

다음 그림에서 보다시피, G1 GC는 바둑판의 각 영역에 객체를 할당하고 GC를 실행한다. 그러다가, 해당 영역이 꽉 차면 다른 영역에서 객체를 할당하고 GC를 실행한다. 즉, 지금까지 설명한 Young의 세가지 영역에서 데이터가 Old 영역으로 이동하는 단계가 사라진 GC 방식이라고 이해하면 된다. G1 GC는 장기적으로 말도 많고 탈도 많은 CMS GC를 대체하기 위해서 만들어 졌다.

G1 GC의 가장 큰 장점은 성능이다. 지금까지 설명한 어떤 GC 방식보다도 빠르다. 하지만, JDK 6에서는 G1 GC를 early access라고 부르며 그냥 시험삼아 사용할 수만 있도록 한다. 그리고 JDK 7에서 정식으로 G1 GC를 포함하여 제공한다.

 

📗 참고

'Programming language > Java' 카테고리의 다른 글

[Java] SOLID란?  (0) 2022.05.16
[Java] 객체지향이란?  (0) 2022.05.16
[Java] JVM, JRE, JDK  (0) 2022.05.12
[Java] JIT 컴파일러란?  (1) 2022.05.09
[Java] JVM이란?  (0) 2022.05.05

댓글