Pink Spider/JVM에서 Heap 메모리의 용도와 GC 튜닝

Created Tue, 01 Apr 2025 13:59:38 +0900 Modified Mon, 08 Dec 2025 08:41:47 +0900
1814 Words 8 min

JVM(Java Virtual Machine)에서 heap 메모리는 자바 애플리케이션이 런타임 시 동적으로 생성하는 객체들이 저장되는 영역입니다. 즉, new 키워드를 사용해서 생성하는 대부분의 객체들이 이 영역에 저장됩니다.


📌 Heap 메모리 구성

Heap은 크게 다음 두 영역으로 나뉩니다:

  • Young Generation (Young Gen): 새로 생성된 객체가 저장되는 곳.
    • Eden: 대부분의 새 객체가 여기에 생성됨.
    • Survivor: Eden에서 살아남은 객체들이 이동.
  • Old Generation (Tenured Gen): Young Gen을 거쳐 오래 살아남은 객체들이 이동하는 공간.

Garbage Collector(GC)는 이 heap 영역의 객체들을 주기적으로 검사해서 더 이상 참조되지 않는 객체는 메모리에서 제거합니다.


💣 Memory Leak 이란?

자바에서는 개발자가 직접 메모리를 해제하지 않기 때문에 메모리 누수가 없다고 오해하기 쉬운데, 참조는 남아있지만 실제로 더 이상 사용하지 않는 객체가 GC의 대상이 되지 못할 때 메모리 누수가 발생합니다.


✅ 메모리 누수를 막기 위한 개발자의 역할

  1. 불필요한 참조 제거

    • 더 이상 필요 없는 객체는 참조를 null로 설정하거나 컬렉션에서 제거해야 GC가 수거할 수 있습니다.
    • 예: List, Map, Set 등에 오래된 데이터를 계속 담아두는 경우.
  2. Static 변수 관리

    • Static은 클래스 로딩 시 생성되어 애플리케이션 종료 전까지 유지됩니다.
    • 필요 이상으로 많은 데이터를 static으로 들고 있으면 GC 대상이 되지 않음.
  3. 리스너나 콜백 해제

    • 등록한 이벤트 리스너, 콜백 등을 적절한 시점에 해제하지 않으면 객체가 계속 참조됨.
  4. WeakReference 사용 고려

    • 캐시처럼 잠깐만 데이터를 보관하고 싶은 경우 WeakReference, SoftReference 등을 사용해 GC 대상이 될 수 있게 설계.
  5. GC 로그 및 Heap Dump 분석

    • jvisualvm, jconsole, MAT(Memory Analyzer Tool) 등을 통해 객체가 어디서 얼마나 생성되고 남아있는지 확인.
  6. Finalizer, Cleaner 피하기

    • finalize()는 예측이 어렵고 GC를 지연시킬 수 있음. 가능하면 AutoCloseable 인터페이스와 try-with-resources 구문을 사용할 것.
  7. ThreadLocal 사용 주의

    • ThreadLocal은 잘못 관리하면 해당 Thread가 종료될 때까지 객체가 해제되지 않음.
    • 사용 후 반드시 remove() 호출할 것.

실전에서 어떻게 메모리 누수가 생기고, 우리가 어떻게 확인하고 해결할 수 있는지 예제


🧪 1. 예제 코드: 메모리 누수가 발생하는 경우

import java.util.ArrayList;
import java.util.List;

public class MemoryLeakExample {
    private static List<Object> leakedList = new ArrayList<>();

    public static void main(String[] args) throws InterruptedException {
        while (true) {
            // 객체를 계속 생성해서 static 리스트에 넣는다
            leakedList.add(new byte[1024 * 1024]); // 1MB
            Thread.sleep(100); // 조금씩 천천히 누수되게
        }
    }
}

🔥 문제

  • leakedList는 static 변수라 GC의 대상이 되지 않고,
  • 내부에 계속 새로운 객체를 추가하므로 Heap 메모리가 점점 차오름.
  • 결국 OutOfMemoryError 발생.

🛠 2. 메모리 누수 확인 방법 (Heap Dump)

🔍 JVM 옵션 추가

프로그램 실행 시 다음 옵션을 넣으면 OOM 발생 시 Heap Dump 파일이 생성됨:

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./heapdump.hprof

🔧 분석 툴: Eclipse MAT (Memory Analyzer Tool)

  1. Eclipse MAT 다운로드
  2. heapdump.hprof 파일 열기
  3. “Leak Suspects Report” 실행
  4. 의심되는 객체/경로 확인
    • 예: leakedList가 수많은 byte[] 객체를 붙잡고 있는 상황 확인 가능

✅ 3. 개선된 코드 예시

import java.util.ArrayList;
import java.util.List;

public class MemoryLeakFixed {
    public static void main(String[] args) throws InterruptedException {
        List<Object> tempList = new ArrayList<>();

        while (true) {
            tempList.add(new byte[1024 * 1024]); // 1MB

            if (tempList.size() > 10) {
                tempList.clear(); // 참조 끊기
            }

            Thread.sleep(100);
        }
    }
}

이렇게 하면 참조가 사라지고, GC가 해당 객체를 수거할 수 있어서 OOM을 방지할 수 있습니다.


GC 튜닝 전략

좋지! GC 튜닝은 JVM 성능 최적화의 핵심 중 하나야. 튜닝은 사용 중인 Garbage Collector 종류, 메모리 구조, 그리고 애플리케이션의 특성에 따라 달라지는데, 아래 내용을 바탕으로 이해하고 튜닝 전략을 세우면 좋아.


🧠 GC 튜닝의 핵심 목표

  • Stop-The-World(STW) 시간을 최소화
  • GC 횟수/빈도 최적화
  • Throughput (총 처리량) 극대화
  • Latency (응답 지연) 최소화

🔧 GC 종류 (Java 11+ 기준)

GC 종류 특징
Serial GC 단일 스레드, 소규모 앱에 적합 (-XX:+UseSerialGC)
Parallel GC 멀티 스레드, 처리량 위주, 기본값 (-XX:+UseParallelGC)
G1 GC 적절한 latency + throughput (-XX:+UseG1GC)
ZGC 매우 낮은 지연 시간, 대용량 Heap에 적합 (-XX:+UseZGC)
Shenandoah GC OpenJDK 계열 저지연 GC (-XX:+UseShenandoahGC)

📌 자주 사용하는 JVM 옵션

# 힙 메모리 크기 설정
-Xms2g               # 초기 Heap 크기
-Xmx2g               # 최대 Heap 크기

# GC 종류 선택
-XX:+UseG1GC         # G1 GC 사용

# GC 로그 출력
-verbose:gc
-Xlog:gc*:gc.log     # Java 9 이상
-XX:+PrintGCDetails  # GC 상세 정보
-XX:+PrintGCDateStamps

# GC 튜닝 예시 (G1GC 기준)
-XX:MaxGCPauseMillis=200           # GC 지연 시간을 200ms 이하로
-XX:InitiatingHeapOccupancyPercent=45  # Old GC 시작 기준점 (%)

🧪 GC 튜닝 전략

1. GC 로그 분석

  • GC 로그를 확인하여 Full GC 빈도, Minor GC 시간, Pause Time 등을 파악.
  • 도구: GCViewer, GCEasy.io

2. Heap 설정 최적화

  • 너무 작은 Heap → 잦은 GC
  • 너무 큰 Heap → GC 시간 증가, 메모리 낭비
  • 예: 서비스 JVM 2~4GB Heap 정도에서 시작, 관측 후 조정

3. 객체 생존 시간에 따라 GC 전략 결정

  • 단명 객체가 많다면 Young 영역을 키우는 게 유리
  • 장기 객체가 많다면 Old 영역 GC가 핵심이 됨

4. Full GC 줄이기

  • 메모리 누수 점검
  • 캐시나 ThreadLocal 관리
  • Heap Dump 분석하여 Old Gen에 무엇이 있는지 확인

5. G1 GC 튜닝 팁

-XX:+UseStringDeduplication       # 중복 문자열 제거
-XX:MaxGCPauseMillis=200          # 최대 GC 지연 시간 설정
-XX:ParallelGCThreads=4           # GC에 사용할 스레드 수
-XX:ConcGCThreads=2               # Concurrent Mark 단계에 사용할 스레드 수

🔍 G1 GC 동작 구조 요약

  • Heap을 여러 Region(영역)으로 쪼갬
  • Young → Old → Humongous (대형 객체) 구조
  • GC는 부분적으로, 병렬로, 동시적으로 실행됨
  • 목표: 큰 Full GC를 피하면서 짧고 자주 GC

📈 튜닝할 때 유용한 도구

  • jstat -gc <pid> : JVM GC 상태 실시간 조회
  • jvisualvm, Java Mission Control: GC 그래프와 Heap 확인
  • VisualGC, HeapHero, GCEasy.io: 시각화