반응형

Garbage Collector(GC)란?

우선 GC가 뭐하는 녀석인지 살펴보겠습니다.

가비지 컬렉터는 가비지 컬렉션(Garbage Collection)을 해주는 녀석입니다.

한글로 하면 쓰레기 수집인데, 도대체 무슨 쓰레기를 수집한단 말인가?

다음은 위키에서 나와있는 가비지 컬렉션에 대한 정의입니다.

 

가비지 컬렉션은 기법 중의 하나로, 프로그램이 동적으로 할당했던 메모리 영역 중에서 필요없게 된 영역을 해제하는 기능이다.

 

그렇습니다. 우리가 JAVA 프로그래밍을 하다보면 new라는 키워드를 자주 사용했을 것입니다.

바로 그 new라는 키워드가 힙 영역에서 동적으로 메모리를 할당해주는 역할을 하고, 그렇게 동적으로 할당된 메모리를 자동으로 해제해주는 것이 GC입니다.

C언어를 사용해봤다면 malloc() 등의 함수로 동적할당한 메모리를 free() 함수를 통해 메모리 해제해야 하던 것을 자동으로 해주는 거라고 생각하면 되겠습니다.

 

언제 동작하는가?

GC도 결국엔 JVM에 올라가기 때문에 기본적으로 런타임에 동작합니다.

가비지 컬렉션이 실행되기에는 몇 가지 조건이 있는데, 다음 조건 중 하나라도 충족되면 JVM은 GC를 실행합니다.

 

1. OS로부터 할당 받은 시스템의 메모리가 부족한 경우

2. 관리하고 있는 힙에서 사용되는 메모리가 허용된 임계값을 초과하는 경우

3. 프로그래머가 직접 GC를 실행하는 경우(JAVA에서는 System.gc()라는 메소드가 있지만 가급적 안 쓰는 것이 좋다.)

 

 

어떻게 동작하는가?

가비지 컬렉션에 관한 알고리즘은 하나만 있는게 아니라 여러 가지가 있습니다. 그럼에도 공통적으로 GC에서 중요한 키워드는 "stop the world" 입니다. 세상을 멈춘다니.. 무슨 말일까요?

이는 JVM이 GC를 실행하는 동안 GC를 실행하는 메소드를 제외한 모든 쓰레드를 일시적으로 정지시킨다는 것을 의미합니다. 그리고 GC가 끝난 후에야 정지 됐던 쓰레드들이 다시 동작합니다.

 

GC는 기본적으로 참조할 수 없는 객체에 대해서 메모리를 해제합니다. 예컨대 C언어에서는 포인터 변수에 null을 대입하는 등의 명시적인 방법으로 객체에 대한 참조를 해제하기도 하는데, 자바에서는 이러한 방식을 하지 않아도 됩니다. 

 

GC를 이해하기 위해서는 먼저 JVM의 물리적인 공간을 이해해야 하는데, JVM은 Young generation 영역Old generation 영역으로 나누어 메모리를 관리합니다.

 

그럼 각 영역은 무슨 기준으로 나누느냐?

이름 그대로 Young 영역은 새롭게 생성된(혹은 생성된지 얼마 안 된) 객체들을 저장하는 공간입니다. 대다수의 객체들이 이 곳에서 생성되었다가 이 곳에서 해제됩니다. Young 영역은 다른 이름으로 Minor GC라고도 합니다.

Old 영역은 Young 영역에서 해제되지 않고 살아남은(?) 객체들을 복사하여 저장하는 곳입니다. Young 영역에 비해 상대적으로 GC가 적게 발생합니다.

 

왜 Young 영역과 Old 영역을 나눌까?

개발 분야에 있으면서 많은 언어나 프레임워크들을 접하다 보면 깨닫게 되는 한 가지가 있습니다. 그건 바로 세상에 '그냥' 만들어진 것이 없다는 것입니다. 세계적인 개발자들은 뭐 하나를 만들어도 합리적이고 타당한 이유로, 그리고 아름답게 만들어내는 것을 좋아하나봅니다.

이처럼 JVM도 아무 이유 없이 Young/Old 영역을 구분 짓는게 아니라, 많은 연구자들은 프로그램에서 새롭게 할당된 영역일수록 금방 해제될 확률이 높다는 관찰을 보고하였기 때문입니다. generation 단위 쓰레기 수집 기법은 이런 특성을 이용하여, 각각의 객체를 할당된 시간에 따라 Young/Old로 구분하여, 각 세대별로 서로 다른 메모리 영역에 객체를 할당합니다.

만약 Young 영역이 꽉 차면, 이 메모리 영역에서 살아남은 객체를 Old 영역으로 옮깁니다. 새로 할당된 영역에서는 대부분의 객체들이 빠르게 해제되고 오래된 영역에서는 객체들이 변하지 않을 확률이 높으므로, 이 기법은 메모리의 일부 영역만을 주기적으로 수집하게 되는 장점이 있습니다.

 

그러면 각 영역은 어떤 알고리즘으로 GC가 동작할까?

먼저 Young 영역을 살펴보면 그 안에서도 1 개의 Eden 영역(에덴 동산할 때 에덴의 의미로 굉장히 상징적입니다), 2 개의 Survivor 영역으로 나뉩니다. 최초에 객체가 생성되면 Eden 영역에 저장되었다가, Eden 영역이 가득차게 되면 Survivor 영역으로 이동하게 됩니다. 이 때 하나의 Survivor만 사용되며, 나머지 하나는 반드시 비어있는 상태여야 합니다. 하나가 다 차면 교체하는 식으로 번갈아가며 쓴다고 이해하면 되겠습니다. 그리고 하나의 Survivor이 가득 찰 때마다 Old 영역으로 객체를 이동합니다.

Eden과 Survivor 영역에서 사용되는 알고리즘으로 bump-the-pointer와 Thread-safe를 위한 TLAB(Thread Local Allocation Buffers) 기법이 있지만, 이 포스팅에서 자세히 다루지는 않겠습니다.

 

이 포스팅을 하기 전에는 막연히 'mark and sweep' 방식을 들은 기억이 있어 그 방식이 아닐까 했는데 아주 오래전(?) 방식이었습니다. 그래도 모르시는 분들을 위해 살짝 짚고 넘어가자면 할당 받은 각 메모리 영역에 1비트씩 남겨서 사용중을 표시(Mark)하고서 GC가 동작하는 동안 각 영역을 검사하여 사용중 표시가 없는 객체들을 해제(Sweep) 해버리는 방식입니다. 이는 처음에 말했던 'stop-the-world'로 인한 성능 저하가 크다는 것입니다.

 

JAVA 11, 12의 GC

그럼 현재 크게 상용화된 버전은 아니지만, 최신 버전이라 할 수 있는 Java 11, 12 버전에서는 어떤 알고리즘으로 동작할까요?

우선 Java 11에서는 EpsilonZ Garbage Collector(ZGC)가 추가되었습니다. 엡실론이라니 이름이 멋있어서 무슨 뜻이 있나 하고 검색해보니 그리스어로 숫자 5라는 의미도 있고, 엡실론 델타 논법이라 해서 수학적 용어로도 쓰인다고 합니다.(그래서 왜 GC 이름이 엡실론인건데..)

 

Epsilon은 메모리 할당은 처리하지만 사용되지 않는 영역에 대해 재활용하지 않습니다. 그리고 기존에 다른 알고리즘의 GC들은 Java Heap 영역이 가득 찼을 경우 OS에 요청하여 추가적으로 Heap 영역을 할당 받았는데, Epsilon의 경우 Java Heap 영역을 모두 소진하게 되면 JVM이 Shut down 됩니다. Epsilon의 목적은 제한된 영역의 메모리 할당을 허용함으로써 최대한 latency overhead를 줄이는 데에 있습니다. Epsilon GC를 사용할 경우 우리가 작성한 어플리케이션이 외부 환경으로부터 고립된 채로 실행되기 때문에  실제 내 어플리케이션이 얼마나 메모리를 사용하는 지에 대한 임계치나 어플리케이션 퍼포먼스 등을 보다 정확하게 측정할 수 있습니다.

 

ZGC는 대량의 메모리를 low-latency로 잘 처리하기 위해 디자인 된 GC 입니다. Oracle에 따르면 multi-tera bytes 크기의 Heap도 관리할 수 있다고 합니다. ZGC는 어플리케이션과 Concurrently하게 동작하는데, Heap Reference를 위해 Load barrier를 사용합니다. 이 Load barrier는 이전 버전에서 사용하던 G1(Garbage First) GC보다 딜레이가 낮습니다. Java 12를 기준으로 했을 때 ZGC의 경우 64bit 운영체제에서만 동작한다고 하는데, 이는 ZGC가 64비트 크기의 Color Point 방식으로 Heap 영역에 있는 객체들을 관리하기 때문입니다. ZGC가 내세우는 최대 장점 중 하나는 'stop-the-world'의 시간이 절대 10ms를 넘지 않는다는 것인데, 구체적인 숫자로 자신있게 얘기하는 것 보니 신뢰해도 좋을 것 같습니다.

 

마지막으로 Java 12에서 도입된 GC는 Shenandoah GC 입니다. Shenandoah는 ZGC와 비슷하게 대량의 메모리 처리에 우수한 퍼포먼스를 내지만 좀 더 많은 옵션을 제공한다는 장점이 있습니다. Shenandoah는 레드 햇에서 개발한 GC인데, 실제로는 ZGC보다 앞선 Java 8부터 개발하기 시작해서 12가 나올 때 release를 한거라고 합니다. 그 때문인지 Java 8, 10 버전에서도 호환이 가능합니다.

 

쓰다보니 내 포스팅 중에 가장 긴 포스팅이 아닌가 싶습니다...ㅎㅎ GC에 대해 공부하기 위해 구글링을 해보니 한국 블로그들은 거의 대부분이 Java 7 버전에 나왔던 G1 방식까지만 나와있길래 최신으로 포스팅 하고자 하는 마음에 오라클 공식문서와 해외 포스팅들을 읽느라 시간이 오래 걸렸지만.. 나름 공들여 작성하고 나니 뿌듯하네요.

무튼, GC라는 녀석이 워낙 프로그래머가 눈치 채지 못하게 뒤에서 자동으로 제 역할을 하던 녀석이다보니 관심을 안두고 있었는데 그런 생각들이 새삼 부끄러웠고, 이제라도 이 쪽은 이 쪽 나름대로 복잡한 알고리즘이 있었고, 계속해서 발전하고 있다는 것을 알게되어 다행이라고 생각합니다.

다양한 GC가 있는 만큼 잘 숙지하고 있어야 내 어플리케이션에 더 적합한 GC를 사용할 수 있겠고, 그것이 나를 수준 높은 프로그래머로 만들어주는 한 걸음이 아닐까 싶습니다.

반응형
반응형