java

[Java] 튜닝의 마지막 단계 GC 알아보기

inhooo00 2025. 2. 5. 03:04

📍GC(Garbage Collection)란?

프로그램이 동적으로 할당했던 메모리 영역 중 필요 없게 된 영역을 알아서 해제하는 것으로 GC는 메모리 관리 기법 중 하나다.
여기서 동적으로 할당했던 메모리 영역은 프로그램 런타임에 사용되는 Heap 영역 메모리를 뜻하고,
필요 없게 된 영역은 어떤 변수도 가리키지 않게 된 영역을 의미한다.

C,C++의 경우 Heap 영역의 메모리를 관리하기 위해 코드 레벨에서 할당 받고, 해제해야 했다. (포인터)
이렇게 수동으로 해제해야하는 경우 할당받은 메모리 영역을 제대로 해제하지 않아 memory leak이 발생하기도 한다.
다행이 필자가 사용하는 Java나 Javascript 같은 언어에서는 동적 메모리 영역 해제를 GC가 대신 해준다.

 

 

 

📍GC가 왜 필요할까?

또 어떻게 보면, 이런 포인터 기반의 메모리 관리 덕분에 C/C++은 성능이나 세밀한 메모리 제어에서 유리하다.

포인터가 무서운 부분도 있지만, 잘 이해하면 강력한 도구가 되니 필요할 때 한 번 써볼 만한 가치가 있다.

 

 

📍GC 알고리즘을 알아보자

1. Reference Counting 

  • heap 영역에 선언된 객체들이 각각 reference count라는 별도의 숫자를 가지고 있음.
  • reference count는 "몇 가지 방법으로 해당 객체에 접근할 수 있는지를 뜻함"
  • 이 방법이 하나도 없다면 GC 대상이 되는 것.
  • 하지만 순환참조 상황이 일어나는 경우도 생겨나며 memory leak이 일어날 수 있음.

2. Mark And Sweep

  • Reference Counting의 순환참조 문제를 해결할 수 있음.
  • 루트에서부터 해당 객체에 접근 가능한지를 해제의 기준으로 삼는다.
  • 루트부터 그래프 순회를 통해 연결된 객체들을 찾아내고(Mark)
  • 연결이 끊어진 객체들은 지우는 방식(Sweep)
  • Java와 JavaScript가 이 방식을 사용한다.
  • 하지만 이 방식은 의도적으로 GC를 실행시켜야 한다. 즉 어느 순간에 어플리케이션이 GC에게 컴퓨터 리소스를 나눠줘야 한다는 말이다.

+ Root Space : 스택 변수, 전역 변수 등 heap 영역 참조를 담은 변수

 

 

 

📍JVM의 GC

 크게 3가지 영역인 JVM의 기본적인 구조를 살펴보자.

 

1. Class Loader

  • 바이트 코드를 읽고, 클래스 정보를 메모리의 Heap/Method 영역에 저장.

 

2. JVM Memory

  • 실행 중인 프로그램의 정보가 올라가 있는 곳.

 

3. Execution Engine(실행엔진)

  • 바이트 코드를 네이티브 코드로 변환시키며 GC를 실행.

 

JVM 실행엔진이 어떻게 GC를 돌리는지 이해하기 위해서는 JVM Memory를 좀 더 깊이 이해해야 한다.

JVM은 OS로부터 메모리를 할당 받은 후, 해당 메모리를 용도에 따라 여러 영역으로 나누어서 관리한다.

모든 스레드가 관여하는 Heap(어플리케이션 실행 중에 생성되는 객체 인스턴스를 저장해 둠), Method area(클래스 구조와 메서드의 코드를 저장해 둠)가 있고,

각 스레드마다 고유하게 행성하고 종료시 사라지는 stack(메서드의 호출을 쌓아둠), pc register(스레드가 실행할 스택 프레임의 주소를 저장해 둠), native method stack(c/c++등의 low level 코드를 실행하는 스택)이 있다.

 

앞서 말했듯, JVM의 GC는 기본적으로 루트에서부터 해당 객체에 접근 가능한지가 해제의 기준이다.

JVM GC의 Root Space는 Steak의 로컬 변수, Method area에 저장된 static 변수, native method stack에 저장된 c/c++로 작성된 JNI이다. (쉽게 말해서 모든 객체들..)

이 시작 지점인 Root Space를 알았으니 Mark-And-Sweep 방식으로 메모리를 관리할 수 있게된다.

 

 

 

📍JVM의 Heap 영역 살펴보기 (그래서 GC를 실행시키는 기준이 뭐지?)

JVM의 Heap 영역은 크게 두 영역으로 나누어진다.

1. Young Generation

  • 이 곳에서 발생하는 GC는 minor GC라고 부름.
  • 이 곳에서는 또 다시 Eden, servival 0, servival 1 영역으로 나누어진다.
  • Eden은 새롭게 생성된 객체들이 할당되는 영역이고, Servival 영역은 minor GC로부터 살아남은 객체들이 존재하는 영역이다. (servival 0,1 둘 중 하나는 비어있어야 한다.)
  • Eden 영역이 객체로 꽉 차게 된다면 minor GC가 발생하며, 루트로부터 Reachable이라 판단된 객체는 Survival 0으로 넘어간다. 이렇게 넘어가게 도니 객체의 숫자(age-bit)는 1로 증가한다. minor GC에서 살아남을 때마다 1씩 증가한다.
  • 그렇게 또 Eden이 꽉 차며 minor GC가 발생하면, 이번에는 Servival 1 영역으로 넘어간다. (Servival 0 에서 Servival 1로 넘어가는 것)
  • 또 Eden이 꽉 차면 Servival 1에서 Servival 0으로 이동. 왔다갔다 하는 것.
  • 그러다가 age-bit가 3(예시)이 된다면? JVM은 이를 보고 오래도록 참조될 객체라고 판단하며여 해당 객체를 Old Generation으로 넘겨준다. 이 행위를 프로모션이라고 한다.
  • 자바 8기준 age-bit가 15가 되면 프로모션이 진행된다.

2. Old Generation

  • 이 곳에서 발생하는 GC는 major GC라고 부름.
  • 시간이 아주 많이 지나면 이 Old Generation이 꽉 차는 날이 온다.
  • 이 때 major GC가 발생하면서 필요 없는 메모리를 비워준다.
  • major GC는 minor GC보다 더 오래 걸린다.
이렇게 Heap 영역을 Young Generation과 Old Generation으로 나눈데는 이유가 있다.
GC 설계자들이 어플리케이션을 분석해보니, 대부분의 객체가 수명이 짧음을 알게 되었다.
GC도 결국 비욜이니 메모리의 특정 부분만 탐색하며 해제하면 더 효율적이겠지?

 

 

 

📍JVM의 Heap 영역 살펴보기 (그럼 어플리케이션 실행과 GC 실행을 어떻게 병행시킬까?)

먼저 Stop the world라는 단어를 이해하고 가자. 
Stop the world란 GC를 실행하기 위해 JVM이 어플리케이션 실행을 멈추는 것.
이 시간을 최소화하는 것이 꽤나 어려운 최적화 작업이다.

 

Serial GC는 하나의 스레드로 GC를 실행시키는 방식이다. 

Stop the world 시간이 상대적으로 더 길며, 싱글 스레드 환경 및 Heap이 매우 작을 때 사용한다.

 

Parallel GC는 여러 개의 스레드로 GC를 실행시키는 방식이다.

java 8이 이 방식을 사용중이며 멀티코어 환경에서 사용한다. 

여러 스레드가 동시에 병렬로 실행해서 빠르게 완료한다.

 

+ CMS GC.. 등등 너무 많음..

 

 

 

📍결론

GC가 일어나는 Heap 영역을 자세하게 알아보았다. 
사실 GC 튜닝은 성능 개선의 최종 단계라고 한다. 객체 생성 자체를 줄이는 코드 레벨에서의 노력이 선행되어야 한다.
memory leak이 일어나는 상황 (https://techblog.woowahan.com/2628/)도 코드 레벨에서의 실수로 일어났다.
GC가 뭔지만 확실히 잡고 가자!
튜닝은 아직 대학생으로 프로젝트나 끄적이는 내가 하기엔 이른 듯하다.. pass~

 

참고 : https://www.youtube.com/watch?v=FMUpVA0Vvjw