Garbage Collector, 제대로 이해하기

2023. 1. 11. 23:57Spring/Java

반응형

소프트웨어 개발자라면 본인의 제품이 메모리를 관리하는 방식에 대한 이해가 필요합니다.  특히  Heap 영역에서 일어나는 동적 메모리 관리는 소프트웨어의 성능을 결정짓는 중요한 요소 중 하나입니다. 컴파일 언어에서 Stack 영역은 컴파일 시간에 그 크기를 가늠할 수 있는 정적 할당과는 달리, 동적 할당은 Runtime 시 Heap영역에서 할당되어 지기 때문에 그 크기가 Application 실행 동안 결정되어 집니다.

 

 

Garbage Collection

C나 C++에서는 개발자들이 동적 할당을 위한 코딩 작업을 하고 난 후, 더 이상 사용되지 않을 때 수동으로 해제해야 합니다. 수동으로 메모리를 해제하는 작업에 대한 휴먼 오류가 발생하기 쉬운데요. 다음과 같은 오류가 발생할 수 있습니다.

 

- 메모리 누수를 유발

- 이미 해제한 메모리에 접근 시도

- 이미 해제한 메모리를 다시 해제 시도

 

GC (Garbage Collector)는 사용되지 않는 메모리를 개발자들의 추가 작업이 없도록 찾아서 없애는 역할을 합니다. 즉, 사용하지 않는(Garbage)들을 수거(Collection)하는 역할을 합니다. 
Java나 Javascript에서 GC를 지원하고 있으며, 프로그램 실행 동안 GC를 수행합니다. 덕분에 개발자들의 코드 작업 없어지고 위에서 살펴본 휴먼 오류를 방지할 수 있죠.


GC가 메모리 관리를 하지만, 개발자들은 메모리가 어떻게 관리되는지, 내부적으로 어떤 동작이 실행되고 있는지를 알 필요가 있습니다. GC가 어떤 일을 하는지 알게 되면, 프로그램에서 사용되는 객체의 생성과 제거를 알 수 있고, 또 이 객체들을 관리할 수 있습니다. 가령, GC 로그를 확인하면서 불필요한 객체를 없애거나 과도한 객체 생성을 효율적인 방법으로 제한하는 등으로 개선시킬 수 있습니다.


짚어보고 갈 점은, GC 라는 약자는 다음의 두 가지 의미를 나타냅니다.
- Garbage Collector

- Garbage Collection

 

즉, 불필요한 메모리를 수집하는 프로그램과 불필요한 메모리를 수집하는 동작 자체를 의미할 수 있습니다.

 

 

Mark And Sweep

Mark And Sweep 알고리즘은 Heap에 생성된 객체 중, 프로그램에서 사용중인 객체 만을 표시(Mark)한 후, 표시되지 않은 객체를 제거(Sweep)합니다. Runtime 시 생성된 객체는 Heap영역에 생성된 뒤에 그 주소 값으로 해당 객체에 접근합니다. 즉, A라는 객체를 Application 동작 중 생성했다면 실제 A라는 객체는 Heap영역에 생성되고, 해당 객체를 참조할 수 있도록 그 주소 값을 Root 영역 메모리에 담아둡니다. 따라서 Application 동작 중 A 객체는 Root 메모리에 저장된 주소 값을 가지고 실제 Heap에 올라온 객체를 참조하여 사용할 수 있습니다. 만약, A라는 객체의 용도가 끝나서 더 이상 사용되지 않는다면, 해당 객체는 참조가 끊기게 되고 이들이 Garbage Collection의 대상이 됩니다.

 

알고리즘의 수행을 두 단계로 나누면 다음과 같습니다.

- 첫 번째, Root영역으로 부터 참조되는 객체를 표시합니다.

영어로 표현하는 느낌을 그대로 가져오면 reached object를 mark한다고 표현할 수 있습니다.

- 두 번째, 표시되지 않는 객체를 제거합니다.

영어의 느낌을 그대로 가져오면 Unmarked Object를 Sweep한다고 표현합니다.

 

 

https://lambda.uta.edu/cse5317/notes/node47.html

 

JVM GC

JVM(Java Virtual Machine)의 Heap 구조를 간략하게 살펴봅니다. 지금부터 살펴볼 Heap 구조는 G1 GC 이전에 사용되던 SerialCG, Parallel GC, Concurrent GC에서 사용하는 구조입니다. 하지만, 이전에 사용하던 명칭들이 현재에도 사용되기 때문에 Garbage Collection의 이해를 목표로 각 영역의 특징을 살펴보겠습니다.

 

JVM의 Heap은 크게 두 영역으로 나눌 수 있습니다. Young Generation과 Old Generation입니다. Young Generation 영역에서는 Young GC라고 부르는 Minor GC가, Old Generation 영역에서는 Old GC 라고 부르는 Major GC가 실행됩니다.

 

해당 글에서는 이후부터 Minor GC, Major GC라고 명시하겠습니다.

 

 

Young generation

Heap 영역의 Young generation에는 영역을 한번 더 나눠 구분 지을 수 있는데, 영역과 역할을 다음과 같습니다.

 

Eden 영역

: 새로운 객체가 생성된 후 할당되는 공간

Eden 영역이 가득차면, minor GC가 실행됩니다. 

 

Survivor 영역

: Minor GC의 실행 이후 살아남은 객체가 할당되는 공간

두 영역 Survivor 0, Survivor 1가 존재합니다. Survivor 공간은 반드시 둘 중 하나는 빈 상태이어야 합니다. Eden 영역에서 Young GC로 살아남는 객체(사용 중인 유효 객체)가 Survivor 0 옮겨진다고 했을 때 Survivor 0는 "From", Survivor 1은 "To"로 명시됩니다. 다음에 발생하는 Minor GC에서 Eden 영역과 Survivor From영역의 살아남는 객체를 Survivor To로 이동합니다.

이하 Garbage Collection 설명에서 다시 언급됩니다.

 

 

Garbage Collecting on JVM

Garbage Collection의 과정을 확인해보겠습니다.

먼저, 새로운 객체가 생성되면 Eden 영역에 배치됩니다.

 

 

 

 

Eden 영역이 가득차서 Minor GC가 실행된다면, 살아남은 객체들은 Survivor 0로 이동되어지며, 각 객체의 age-bit가 1로 증가합니다. 

 

 

 

 

다시 한 번, Eden 영역이 가득찬 후 Minor GC가 실행되면, 이 번에는 Survivor 1로 이동하며 age-bit가 하나 더 증가합니다. 이렇게 Survivor 공간은 번갈아 가면서 From공간과 To 공간으로 간주되어 살아남은 객체들을 담습니다.

 

 

 

 

이렇게 계속해서 살아남는 객체들에 대해 Minor GC를 진행하다가 age-bit가 임계값에 도달하게 되면, JVM GC는 이 객체들을 Old Generation 영역으로 옮기며 이 과정을 Promotion이라고 부릅니다. Java 8의 Parallel GC에서는 이 임계값이 15이며, age-bit가 15를 넘었을 때 Promotion이 진행됩니다. 

 

 

 

시간이 지나고 Old Generation 이 가득차게되면, 이번에는 Major GC가 발생하여 필요없는 메모리를 제거합니다. 보통 Minor GC보다 Major GC의 실행 시간이 더 오래 걸립니다. 

 

 

 

 

그렇다면, 왜 Young generation과 Old generation 영역을 굳이 구분할까요? 그 이유는 GC 설계자들은 대부분의 객체의 수명이 굉장히 짧다는 것을 고려했기 때문입니다. 대부분의 객체들이 생성되고 사라지기 때문에, 그렇지 않은 객체들과 구분하는 것이고, 더 빠르게 제거하게끔 Minor GC로 먼저 없애려는 것입니다.

 

 

이제부터 시간에 따라 등장한 Garbage Collection에 대해 소개할텐데요. 

그 전에 반드시 알고가야할 개념인 “Stop the World”를 알아보겠습니다.

* STW: “Stop the World”는 JVM에서 특정 GC를 실행하기 위해 Application을 잠시 멈추는 것을 의미합니다.

 

 

이제 JVM에서 사용되어 오는 몇 가지 GC에 대해 알아보도록 하겠습니다.

 

 

1. Serial GC

Serial GC는 단 하나의 스레드를 가지고 Garbage Collection을 진행합니다. 오직 하나의 스레드를 사용하기 때문에 Stop the World 대기 시간이 상당히 오래걸립니다. Serial GC는 싱글 스레드의 Heap 사이즈가 꽤 작았을 때 등장했습니다. 

한 번 Serial GC가 발생하면, Application이 지연 시간을 오래 갖게 되면서 사용자들 또한 오랜 시간 대기해야 했습니다. 이 대기 시간을 줄이기 위해 지연을 늦춰야 했고, Stop the World 대기 시간을 줄이려는 노력이 현재까지 계속되어 오고 있습니다.

 

2. Parallel CG

Parallel GC는 여러 스레드를 사용하여 Garbage Collection을 진행합니다. 멀티 스레드를 사용하기 때문에, Java 8에서 Default로 사용되었습니다. 이전 세대의 GC인 Serial GC 보다 Stop The World 대기 시간이 더욱 짧아졌습니다. Parallel CG는 멀티 코어 환경에서 사용될 수 있습니다.

 

 

멀티 스레드를 통해서 Stop The World 대기 시간이 짧아졌지만, 조금 더 개선할 여지가 있었고 계속해서 이 대기 시간을 줄이려 노력했습니다. 그래서 다음 세대인 CMS GC가 등장하게 됩니다.

 

3. CMS GC

CMS GC는 Java 9 이후로 더 이상 사용되지 않으며 Java 14에서 G1GC를 위해 완전히 제거되었습니다.

 

Concurrent Mark Sweep (CMS) GC는 Garbage Collection 대기 시간 단축과 Garbage Collector와 리소스를 공유할 수 있게 설계되었습니다. 다른 Collector들과 동일하게 Minor 그리고 Major Collection을 실행합니다. 

 

 

Major Collection을 실행하면서, CMS Collector는 시작 할 때 짧은 기간 동안 모든 Application을 멈추고 Collection 중간에 다시 한 번 대기합니다. 보통 두 번째 대기는 첫 번째 보다 더 긴 대기 시간을 갖곤 하며, 두 Collection 동안 멀티 스레드를 사용합니다. 

Minor Collection은 Major Collection 중간에 실행될 수 있으며, Parallel GC와 비슷하게 Minor Collection 동안 Application의 스레드가 멈추는 Stop The World가 발생합니다.

 

 

4. G1 GC

G1 collector는 많은 양의 메모리를 포함한 멀티 프로세서 위에서 실행할 수 있으며, 이로 인해 고가용성과 전반적인 대기 시간을 줄이는데 효과적입니다. G1 GC는 Java 9이후부터 Default로 사용되어 왔습니다.

 

 

https://docs.oracle.com/javase/9/gctuning/garbage-first-garbage-collector.htm#JSGCT-GUID-ED3AB6D3-FD9B-4447-9EDF-983ED2F7A573

 

 높은 처리량을 제공하면서 GC 대기 기간을 줄이려는 목표를 맞춰나갔는데, 가령 모든 Heap 영역의 Mark 작업을 병행적(Conccurent)하게 처리합니다. G1 GC는 다음 포스팅에서 조금 더 자세히 다루겠습니다.

 

 

 

 

CMS, G1 GC, ZGC에 대한 내용  보강 중입니다. (업데이트가 안되면... 저를 재촉해주세요...ㅎㅎ)

 

 

 

반응형

Backend Software Engineer

Gyeongsun Park