JVM GC(Garbage Collection) 이란?
GC란 무엇인가?
GC는 Garbage Collection의 약자로 Java 언어의 중요한 특징중의 하나입니다. GC는 Java Application에서 사용하지 않는 메모리를 자동으로 수거하는 기능을 말합니다.
예전의 전통적인 언어 C등의 경우 malloc, free등을 이용해서 메모리를 할당하고, 일일이 그 메모리를 수거해 줘야 했습니다. 그러나 Java 언어에서는 GC 기술을 사용함에 따라서 개발자로 하여금 메모리 관리에서 부터 좀더 자유롭게 해주었습니다.
GC의 동작 방법은 어떻게 되는가?
1. JVM 메모리 영역
GC의 동작 방법을 이해하기 위해서는 Java의 메모리 구조를 먼저 이해할 필요가 있습니다. 일반적으로 Application에서 사용되는 객체는 오래 유지 되는 객체보다, 생성되고 얼마 안 있어서 사용되지 않는 경우가 많습니다. <그림 1 참조>
[그림 1]. 메모리 foot print
그래서 Java에서는 크게 두가지 영역으로 메모리를 나누는데 Young 영역과 Old 영역이 그것입니다. Young 영역은 생긴지 얼마 안된 객체들을 저장하는 장소이고, Old영역은 생성 된지 오래된 객체를 저장하는 장소입니다. 각 영역의 성격이 다른 만큼 GC의 방법도 다릅니다.
먼저 Java의 메모리 구조를 살펴보겠습니다.
[그림 2]. Java 메모리 구조
Java의 메모리 영역은 앞에서 이야기한 두 영역 (Young 영역,Old 영역)과 Perm 영역 이렇게 3가지로 영역으로 구성됩니다.
[표 1]. Java 메모리 영역
2. GC 알고리즘
그러면 이 메모리 영역을 JVM이 어떻게 관리하는지에 대해서 알아보겠습니다.
JVM은 New/Young 영역과, Old영역 이 두 영역에 대해서만 GC를 수행합니다. Perm영역은 앞에서 설명했듯이 Code가 올라가는 부분이기 때문에, GC가 일어날 필요가 없습니다. Perm영역은 Code가 모두 Load되고 나면 거의 일정한 수치를 유지합니다.
- Minor GC
먼저 New/Young영역의 GC방법을 살펴보면 New/Young 영역의 GC를 Minor GC라고 부르는데, New/Young영역은 Eden과 Survivor라는 두 가지 영역으로 또 나뉘어 집니다. Eden영역은 Java 객체가 생성되자 마자 저장이 되는 곳입니다. 이렇게 생성된 객체는 Minor GC가 발생할때 Survivor 영역으로 이동됩니다.
Survivor 영역은 Survivor 1과 Suvivor2 영역 두 영역으로 나뉘어 지는데, Minor GC가 발생하면 Eden과 Survivor1에 Alive되어 있는 객체를 Suvivor2로 복사합니다. 그리고 Alive되어 있지 않는 객체는 자연히 Suvivor1에 남아있게 되고, Survivor1과 Eden영역을 Clear합니다. (결과적으로 Alive된 객체만 Survivor2로 이동한 것입니다.)
다음 번 Minor GC가 발생하면 같은 원리로 Eden과 Survivor2영역에서 Alive되어 있는 객체를 Survivor1에 복사합니다. 계속 이런 방법을 반복적으로 수행하면서 Minor GC를 수행합니다.
이렇게 Minor GC를 수행하다가, Survivor영역에서 오래된 객체는 Old영역으로 옮기게 됩니다. 이런 방식의 GC 알고리즘을 Copy & Scavenge라고 합니다. 이 방법은 매우 속도가 빠르며 작은 크기의 메모리를 Collecting하는데 매우 효과적입니다. Minor GC의 경우에는 자주 일어나기 때문에, GC에 소요되는 시간이 짧은 알고리즘이 적합합니다.
이 내용을 그림을 보면서 살펴보도록 하겠습니다.
[그림 3-1]. 1st Minor GC
Eden에서 Alive된 객체를 Suvivor1으로 이동하고. Eden 영역을 Clear합니다.
[그림 3-2]. 2nd Minor GC
Eden영역에 Alive된 객체와 Suvivor1영역에 Alive된 객체를 Survivor 2에 copy하고.
Eden영역과 Suvivor2영역을 clear합니다.
[그림 3-3]. 3rd Minor GC
객체가 생성된 시간이 오래 지나면 Eden과 Suvivor영역에 있는 오래된 객체들을 Old 영역으로 이동합니다.
- Full GC
Old 영역의 Garbage Collection을 Full GC라고 부르며, Full GC에 사용되는 알고리즘은 Mark & Compact라는 알고리즘을 이용합니다. Mark & Compact 알고리즘은 전체 객체들의 reference를 쭉 따라 가면서 reference가 연결되지 않는 객체를 Mark합니다. 이 작업이 끝나면 사용되지 않는 객체를 모두 Mark가 되고, 이 mark된 객체를 삭제합니다.<그림 4 참고> (실제로는 compact라고 해서, mark된 객체로 생기는 부분을 unmark된 즉 사용하는 객체로 메꾸어 버리는 방법입니다.)
Full GC는 매우 속도가 느리며, Full GC가 일어나는 도중에는 순간적으로 Java Application이 멈춰 버리기 때문에, Full GC가 일어나는 정도와 Full GC에 소요되는 시간은 Application의 성능과 안정성에 아주 큰 영향을 줍니다.
[그림 4]. Full GC
GC가 왜 중요한가?
Garbage Collection중에서 Minor GC의 경우 보통 0.5초 이내에 끝나기 때문에 큰문제가 되지 않습니다. 그러나 Full GC의 경우 보통 수초가 소요가 되고, Full GC동안에는 Java Application이 멈춰버리기 때문에 문제가 될 수 있습니다.
예를 들어 게임 서버와 같은 Real Time Server를 구현을 했을 때, Full GC가 일어나서 5초 동안 시스템이 멈춘다고 생각해보면, 또 일반 WAS에서도 5~10초 동안 멈추면, 멈추는 동안의 사용자의 Request가 Queue에 저장되었다가 Full GC가 끝난 후에 그 요청이 한꺼번에 들어오게 되면 과부하에 의한 여러 장애를 만들 수 있습니다.
그래서 원할 한 서비스를 위해서는 GC를 어떻게 일어나게 하느냐가 시스템의 안정성과 성능에 큰 변수로 작용할 수 있습니다.
다양한 GC 알고리즘
앞에서 설명한 기본적인 GC방법 (Scavenge 와 Mark and compact)이외에 JVM에서는 좀더 다양한 GC 방법을 제공하고 그 동작방법이나 사용방법도 틀리다. 이번에는 다양한 GC 알고리즘에 대해서 알아보면. 현재 (JDK 1.4)까지 나와 있는 JVM의 GC방법은 크게 아래 4가지를 지원하고 있습니다.
Default Collector
Parallel GC for young generation (from JDK 1.4 )
Concurrent GC for old generation (from JDK 1.4)
Incremental GC (Train GC)
1. Default Collector
이 GC 방법은 앞에서 설명한 전통적인 GC방법으로 Minor GC에 Scavenge를, Full GC에 Mark & compact 알고리즘을 사용하는 방법입니다. 이 알고리즘에는 이미 앞에서 설명했기 때문에 별도의 설명을 하지는 않습니다.
JDK 1.4에서부터 새로 적용되는 GC방법은 Parallel GC와 Concurrent GC 두 가지 방법이 있습니다. Parallel GC는 Minor GC를 좀더 빨리하게 하는 방법이고 (Throughput 위주) Concurrent GC는 Full GC시에 시스템의 멈춤(Pause)현상을 최소화하는 GC방법입니다.
2. Parallel GC
JDK1.3까지 GC는 하나의 Thread에서 이루어집니다. Java가 Multi Thread환경을 지원함에도 불구하고, 1 CPU에서는 동시에 하나의 Thread만을 수행할 수 밖에 없기 때문에, 예전에는 하나의 CPU에서만 GC를 수행했지만, 근래에 들어서 하나의 CPU에서 동시에 여러 개의 Thread를 실행할 수 있는 Hyper Threading기술이나, 여러 개의 CPU를 동시에 장착한 HW의 보급으로 하나의 HW Box에서 동시에 여러 개의 Thread를 수행할 수 있게 되었습니다.
JDK 1.4부터 지원되는 Parallel GC는 Minor GC를 동시에 여러 개의 Thread를 이용해서 GC를 수행하는 방법으로 하나의 Thread를 이용하는 것보다 훨씬 빨리 GC를 수행할 수 있습니다.
[그림 7]. Parallel GC 개념도
<그림 7> 을 보면 왼쪽의 Default GC방법은 GC가 일어 날 때 Thread들이 작업을 멈추고, GC를 수행하는 thread만 gc를 수행합니다. (그림에서 파란영역), Parallel GC에서는 여러 thread들이 gc를 수행이 가능하기 때문에, gc에 소요되는 시간이 낮아집니다.
Parallel GC가 언제나 유익한 것은 아니다. 앞에서도 말했듯이 1CPU에서는 동시에 여러 개의 thread를 실행할 수 없기 때문에 오히려 Parallel GC가 Default GC에 비해서 느립니다. 2 CPU에서도 Multi thread에 대한 지원이나 계산 등을 위해서 CPU Power가 사용되기 때문에, 최소한 4CPU의 256M 정도의 메모리를 가지고 있는 HW에서 Parallel GC가 유용하게 사용됩니다.
Parallel GC는 크게 두 가지 종류의 옵션을 가지고 있는데,Low-pause 방식과 Throughput 방식의 GC방식이 있습니다.
Solaris 기준에서 Low-pause Parallel GC는 -XX:+UseParNewGC 옵션을 사용합니다. 이 모델은 Old 영역을 GC할때 다음에 설명할 Concurrent GC방법과 함께 사용할 수 있습니다. 이 방법은 GC가 일어날때 빨리 GC하는것이 아니라 GC가 발생 할 때 Application이 멈춰지는 현상(pause)를 최소화하는데 역점을 두었습니다
Throughput 방식의 Parallel GC는 -XX:+UseParallelGC (Solaris 기준) 옵션을 이용하며 Old 영역을 GC할때는 Default GC (Mark and compact)방법만을 사용하도록 되어 있습니다. Minor GC가 발생했을때, 되도록이면 빨리 수행하도록 throughput에 역점을 두었습니다.
그 외에도 ParallelGC를 수행 할 때 동시에 몇 개의 Thread를 이용하여 Minor영역을 Parallel GC할지를 결정할 수 있는데, -XX:ParallelGCThreads= 옵션을 이용하여 Parallel GC에 사용되는 Thread의 수를 지정할 수 있습니다.
3. Concurrent GC
앞에서도 설명했듯이, Full GC즉 Old 영역을 GC하는 경우에는 그 시간이 길고 Application이 순간적으로 멈춰버리기 때문에, 시스템 운용에 문제가 됩니다. 그래서 JDK1.4부터 제공하는 Concurrent GC는 기존의 이런 Full GC의 단점을 보완하기 위해서 Full GC에 의해서 Application이 멈추어 지는 현상을 최소화 하기 위한 GC방법입니다.
Full GC에 소요되는 작업을 Application을 멈추고 진행 하는 것이 아니라, 일부는 Application이 돌아가는 단계에서 수행하고, 최소한의 작업만을 Application이 멈췄을때 수행하는 방법으로 Application이 멈추는 시간을 최소화합니다.
[그림 8]. Concurrent GC 개념도
그림 8에서와 같이 Application이 수행중일때(붉은 라인) Full GC를 위한 작업을 수행합니다. (Sweep,mark) Application을 멈추고 수행하는 작업은 일부분 (initial-mark, remark 작업)만을 수행하기 때문에, 기존 Default GC의 Mark & Sweep Collector에 비해서 Application이 멈추는 시간이 현저하게 줄어듭니다.
Solaris JVM에서는 -XX:+UseConcMarkSweepGC Parameter를 이용해 세팅합니다.
4. Incremental GC (Train GC)
Incremental GC또는 Train GC라고도 불리는 GC방법은 JDK 1.3에서부터 지원된 GC방법입니다. 앞에서 설명한 Concurrent GC와 비슷하게, 의도 자체는 Full GC에 의해서 Application이 멈추는 시간을 줄이고자 하는데 있습니다. Incremental GC의 작동방법은 간단합니다. Minor GC가 일어날때 마다 Old영역을 조금씩 GC를 해서 Full GC가 발생하는 횟수나 시간을 줄이는 방법입니다.
[그림 9]. Incremental GC 개념도
그림 9에서 보듯이. 왼쪽의 Default GC는 Full GC가 일어난 후에나 Old 영역이 Clear됩니다. 그러나, 오른쪽의 Incremental GC를 보면 Minor GC가 일어난 후에, Old 영역이 일부 Collect된 것을 볼 수 있고. Incremental GC를 사용하는 방법은 JVM 옵션에 -Xinc 옵션을 사용하면 됩니다. Incremental GC는 많은 자원을 소모하고, Minor GC를 자주 일으키고, 그리고 Incremental GC를 사용한다고 Full GC가 없어지거나 그 횟수가 획기적으로 줄어드는 것은 아닙니다. 오히려 느려지는 경우가 많다. 필히 테스트 후에 사용하도록 해야 합니다.
※ Default GC이외의 알고리즘은 Application의 형태나 HW Spec(CPU수, Hyper threading 지원 여부), 그리고 JVM 버전(JDK 1.4.1이냐 1.4.2냐)에 따라서 차이가 매우 큽니다. 이론상으로는 실제로 성능이 좋아 보일 수 있으나, 운영 환경에서는 여러 요인으로 인해서 기대했던 것 만큼의 성능이 안 나올 수 있기 때문에, 실 환경에서 미리 충분한 테스트를 거쳐서 검증한 후에 사용해야 합니다.