[JAVA] 공식문서로 공부하는 Garbage Collection - Ch 1. 가비지 컬렉션 튜닝 소개

예름·2025년 3월 10일
5

Java

목록 보기
3/9
post-thumbnail

Oracle의 공식문서 HotSpot Virtual Machine Garbage Collection Tuning Guide을 참고했습니다.

📍 가비지 컬렉션(Garbage Collection)이란?
📍 가비지 컬렉터(Garbage Collector)가 필요한 이유?
📍 가비지 컬렉터(Garbage Collector) 선택 기준

📍 HotSpot VM 가비지 컬렉션 튜닝 가이드

Java 애플리케이션은 다양한 환경에서 실행될 수 있다. 작은 데스크톱 애플리케이션부터 대형 서버에서 운영되는 웹 서비스까지 폭넓게 활용되기 때문에, Java HotSpot 가상 머신(HotSpot VM)은 여러 가지 가비지 컬렉터(GC)를 제공하여 서로 다른 요구 사항을 충족할 수 있도록 한다.

일반적으로 Java SE는 실행 환경에 따라 가장 적절한 가비지 컬렉터를 자동으로 선택하지만, 특정 애플리케이션의 성능 목표에 맞춰 최적화하려면 직접 GC를 선택하고 튜닝해야 한다.

이 문서에서는 GC의 기본 개념과 다양한 GC 알고리즘의 특성을 설명하고, 각 GC의 선택 기준과 튜닝 방법을 다룬다.

❓JVM? HotSpot VM?

JVM은 Java 프로그램을 실행하기 위한 가상 머신이다.

자바 코드(바이트 코드)를 운영체제와 하드웨어에 독립적으로 실행할 수 있도록 해준다.

JVM의 주요 역할

  1. 바이트코드 실행: .class 파일(바이트코드)을 읽고 해석하여 실행한다.
  2. 메모리 관리: 힙(heap), 스택(stack) 등을 관리하고, GC를 통해 불필요한 객체를 정리한다.
  3. 플랫폼 독립성 제공: “Write Once, Run Anywhere(한 번 작성하면 어디서든 실행 가능)“을 가능하게 한다.

HotSpot VM은 JVM의 구현체 중 하나이다.

즉, HotSpot VM은 하나의 JVM으로 Oracle과 OpenJDK에서 기본적으로 제공하는 JVM이다.

HotSpot VM의 특징

  1. JIT(Just-In-Time) 컴파일러 제공
    바이트코드를 실행할 때, 필요하면 즉시 네이티브 코드(기계어)로 변환하여 성능을 향상시킴.
  2. 가비지 컬렉션(GC) 지원
    다양한 GC 알고리즘(Serial, Parallel, G1, ZGC 등)을 제공하여 메모리 관리 최적화.
  3. 성능 최적화 기술 포함
    런타임에 코드 실행 패턴을 분석하고, 자주 실행되는 코드를 최적화하여 속도를 높임.

다른 JVM 구현체로는 OpenJ9, GraalVM, Zulu, Corretto, Liberica 등이 있다.

❓그럼 JVM은 인터페이스인가?

JVM은 인터페이스라기보다는 명세(Specification) + 구현(Implementation)을 포함하는 개념이다.

명세 자체는 구현이 아니라 설계도이다.

JVM이 “자동차”라면, HotSpot VM은 “특정 브랜드(예: BMW) 자동차”라고 볼 수 있다.


🔎 가비지 컬렉터(Garbage Collector)란?

가비지 컬렉터란 애플리케이션의 동적 메모리 할당을 자동으로 관리하는 시스템이다.

GC는 다음과 같은 작업을 수행한다.

1️⃣ 운영체제로부터 메모리를 할당하고 반납한다.

프로그램이 실행되면 운영체제(OS)는 Java 가상 머신(JVM)에 일정량의 메모리를 할당한다.

JVM은 이 메모리를 힙(heap) 메모리로 사용하며, 애플리케이션이 필요할 때마다 객체를 생성하여 여기에 저장한다.

GC는 이 힙 영역에서 사용되지 않는 객체를 찾아 제거하고, 필요할 경우 운영체제에 메모리를 반환하기도 한다.

❓ 왜 운영체제에서 직접 메모리를 관리하지 않는가?

운영체제는 프로세스 단위로 메모리를 관리하지만, JVM은 애플리케이션이 생성하는 개별 객체 단위로 관리해야 하기 때문이다.

2️⃣ 애플리케이션이 요청하는 대로 메모리를 할당한다.

Java 애플리케이션이 new 키워드를 사용하여 객체를 생성하면, JVM은 힙 영역에서 메모리를 찾아 해당 객체에 할당한다.

만약 메모리가 부족하다면 GC가 동작하여 불필요한 객체를 제거하고, 새로운 객체를 위한 공간을 확보한다.

❓ 왜 GC가 없으면 문제가 되는가?

애플리케이션이 계속해서 객체를 생성하면 결국 메모리가 가득 차고, 더 이상 객체를 생성할 수 없는 상황이 발생하기 때문이다.

3️⃣ 어떤 메모리가 여전히 애플리케이션에서 사용되고 있는지 판단한다.

GC는 객체가 여전히 사용 중인지 아닌지를 판단해야 한다.

이를 위해 Java는 참조(reference) 개념을 사용한다.

어떤 객체에 대한 참조가 남아 있다면 해당 객체는 아직 필요하다고 간주되며, 참조가 하나도 남아 있지 않다면 GC가 해당 객체를 제거할 수 있다.

❓ 왜 참조(reference)로 판단하는가?

단순히 객체가 오래되었다고 삭제하면, 여전히 필요한 객체까지 삭제할 위험이 있기 때문이다.

4️⃣ 더 이상 사용되지 않는 메모리를 회수하여 애플리케이션이 재사용할 수 있도록 한다.

GC는 사용되지 않는 객체를 삭제하고, 해제된 메모리를 새로운 객체 생성을 위해 재사용할 수 있도록 한다.

이 과정이 자동으로 이루어지기 때문에 개발자는 메모리 관리를 직접 신경 쓸 필요가 없다.

❓ 왜 개발자가 직접 메모리를 해제하지 않는가?

개발자가 수동으로 메모리를 관리하면 실수가 발생할 가능성이 크기 때문이다.

또한, GC가 자동으로 수행하면 코드가 간결해지고 유지보수가 쉬워진다.

✅ JVM의 메모리 구조

  1. 힙(Heap): 동적으로 생성된 객체를 저장
  2. 메소드 영역(Method Area): 클래스 정보와 메소드 코드 등 저장
  3. 스택(Stack): 메소드 호출 시 로컬 변수와 호출 정보를 저장
  4. 네이티브 메소드 스택: 네이티브 메소드 호출 시 사용되는 메모리
  5. PC 레지스터: 각 스레드의 명령어 주소를 추적

→ 가비지 컬렉션은 힙 영역을 관리

✅ GC의 역할과 작동 방식

GC는 애플리케이션이 실행되는 동안 동적으로 생성된 객체를 관리하고, 필요하지 않은 객체를 제거하는 역할을 한다.

GC가 없다면 개발자가 직접 메모리를 할당하고 해제해야 하는데, 이는 실수로 이어질 가능성이 크다.

예를 들어, C와 같은 언어에서는 사용한 메모리를 직접 해제해야 하며, 해제하지 않으면 메모리 누수(memory leak) 가 발생할 수 있다.

반대로, 잘못 해제하면 더블 프리(double free) 오류댕글링 포인터(dangling pointer) 오류가 발생할 수도 있다.

댕글링 포인터(dangling pointer) 오류: 해제된 메모리를 가리키고 있는 포인터

❓ 왜 GC가 필요한가?

개발자가 직접 메모리를 관리하는 것은 어렵고 위험하기 때문이다.

메모리 누수와 같은 문제를 자동으로 해결하기 위해 GC가 등장했다.

✅ GC의 성능 최적화 기법

HotSpot 가비지 컬렉터는 여러 가지 기술을 활용하여 GC 성능을 향상시킨다.

  • 세대별 수집(Generational Scavenging) 및 객체 노화(Aging) 기법
    • 메모리를 관리할 때, Young Generation과 Old Generation으로 구분하여 Young Generation에서 주로 GC를 수행함으로써 성능을 개선한다.
    • Young Generation에서는 대부분의 객체가 빠르게 생성되고 소멸하므로, 자주 GC를 실행해 불필요한 객체를 빠르게 제거한다.
      - 살아남은 객체는 Old Generation으로 이동하며, Old Generation에서의 GC는 상대적으로 덜 빈번하게 실행된다.
  • 멀티스레딩을 활용한 병렬 처리(Parallel Processing)
    • 여러 개의 스레드를 활용하여 GC 작업을 병렬로 수행함으로써 성능을 향상시킨다.
    • 일부 GC 알고리즘은 애플리케이션과 동시에 동작하는 동시(concurrent) GC 기능을 제공하여 애플리케이션 성능 저하를 최소화한다.
  • 객체 압축(Compaction)
    • GC가 실행될 때, 살아남은 객체를 메모리 공간에서 연속적으로 배치하여 단편화를 줄이고 메모리 할당 속도를 개선한다.
    • 단편화가 심해지면 메모리 할당 속도가 저하될 수 있기 때문에, 압축 과정을 통해 큰 연속된 메모리 블록을 확보한다.

🔎 가비지 컬렉터 선택이 중요한 이유

가비지 컬렉터를 사용하면 개발자가 직접 메모리를 관리하지 않아도 되므로, 메모리 누수(memory leak)나 잘못된 해제(double free)와 같은 문제를 방지할 수 있다.

그러나, GC가 실행되는 동안 애플리케이션의 실행이 일시적으로 멈출 수 있으며, 이로 인해 성능 저하가 발생할 수도 있다.

따라서, GC의 선택이 중요한 경우가 있다.

✅ GC 선택이 필요 없는 경우

일반적인 애플리케이션은 적절한 GC를 자동으로 선택해도 문제없이 실행된다.

예를 들어, 가비지 컬렉션으로 인해 가끔 짧은 정지 시간이 발생하더라도 성능에 큰 영향을 주지 않는다면 기본 설정을 유지해도 된다.

✅ GC 선택이 중요한 경우

다음과 같은 조건에서는 적절한 GC를 선택하고 튜닝하는 것이 성능에 큰 영향을 미칠 수 있다.

  1. 대량의 데이터를 처리하는 애플리케이션
  • 수 기가바이트(GB) 이상의 데이터를 다루는 애플리케이션은 GC의 성능이 중요한 요소가 된다.
  • 불필요한 GC 실행을 줄이고, 메모리 할당과 회수를 효율적으로 수행해야 한다.
  1. 멀티스레드 환경에서 실행되는 애플리케이션
  • 많은 스레드를 활용하는 서버 애플리케이션의 경우, GC가 병목이 될 가능성이 높다.
  • 멀티스레드를 지원하는 GC를 선택하면 성능을 개선할 수 있다.
  1. 높은 트랜잭션 처리량을 요구하는 애플리케이션
  • 금융 시스템이나 대규모 웹 서비스처럼 높은 TPS(Transactions Per Second)를 요구하는 경우, GC로 인한 지연 시간이 문제될 수 있다.
  • 짧은 응답 시간과 낮은 지연 시간을 유지하려면 적절한 GC 튜닝이 필요하다.

🔎 GC의 성능과 확장성 문제

✅ Amdahl의 법칙과 GC의 영향

Amdahl의 법칙(Amdahl’s Law)에 따르면, 애플리케이션의 성능 향상은 병렬화할 수 없는 코드에 의해 제한된다.

즉, 병렬화가 가능한 부분이 많더라도, GC와 같은 순차적 작업이 병목이 되면 전체 성능이 저하될 수 있다.

아래 그래프는 GC가 전체 실행 시간의 1% 또는 10%를 차지할 때, CPU 코어 수가 증가함에 따라 성능이 얼마나 저하되는지를 보여준다.

  • GC가 전체 실행 시간의 1%를 차지할 경우
    • 단일 프로세서에서 실행할 때는 큰 문제가 없지만, 32개 이상의 프로세서를 사용할 때는 전체 성능이 20% 이상 감소한다.
  • GC가 전체 실행 시간의 10%를 차지할 경우
    • 멀티코어 환경에서 GC가 성능을 저해하는 주된 원인이 되며, 32개 이상의 프로세서에서 75% 이상의 성능이 감소할 수 있다.

이를 통해, 작은 시스템에서는 GC로 인한 문제를 쉽게 간과할 수 있지만, 대규모 시스템에서는 GC 성능이 중요한 요소가 된다는 것을 알 수 있다.

따라서, 시스템의 규모가 커질수록 적절한 GC 선택과 튜닝이 필요하다.


🔎 가비지 컬렉터 선택 기준

1️⃣ Serial GC

  • 단일 스레드로 동작하는 간단한 GC.
  • 작은 애플리케이션(약 100MB 이하의 힙)을 실행하는 데 적합.
  • 단일 프로세서 환경에서 적절한 성능을 제공.
  • 왜? → 병렬 처리를 지원하지 않기 때문에 멀티코어 환경에서 비효율적일 수 있다.

2️⃣ Parallel GC

  • 여러 개의 스레드를 활용하여 GC 작업을 병렬로 수행.
  • 서버 환경에서 기본적으로 선택되는 GC.
  • 짧은 응답 시간보다는 높은 처리량(Throughput)을 목표로 함.

3️⃣ G1 (Garbage-First) GC

  • Java 9부터 기본 GC로 사용.
  • 힙을 여러 개의 영역으로 나누고, 회수할 필요가 큰 영역을 우선적으로 정리.
  • 낮은 지연 시간(Low Latency)과 일정한 응답 시간을 목표로 하는 애플리케이션에 적합.

4️⃣ ZGC 및 Shenandoah GC

  • 매우 낮은 응답 시간을 목표로 하는 최신 GC.
  • GC로 인한 정지 시간을 밀리초(ms) 단위로 최소화.
  • 대규모 시스템 및 실시간 애플리케이션에 적합.

💡 결론

  • 작은 애플리케이션 → Serial GC
  • 높은 처리량(Throughput) 요구 → Parallel GC
  • 낮은 지연 시간(Low Latency) 요구 → G1 GC
  • 초저지연(Real-time Low Latency) 요구 → ZGC, Shenandoah GC

각 애플리케이션의 요구 사항에 맞춰 적절한 GC를 선택하고 튜닝하는 것이 중요하다.


참고문헌
🔗 Oracle 공식문서
🔗 JVM 메모리 구조

profile
안정적인 쳇바퀴를 돌리는 삶

2개의 댓글

comment-user-thumbnail
2025년 3월 11일

프로젝트 사후 관리 잘 부탁드립니다!

1개의 답글