가비지 컬렉션(Garbage Collection)이란 무엇인가요?

김상욱·2024년 11월 14일

가비지 컬렉션(Garbage Collection)이란 무엇인가요?

프로그래밍 언어에서 메모리 관리를 자동화하는 기능. 개발자가 명시적으로 메모리를 해제하지 않아도, 더 이상 명시적으로 메모리를 해제하지 않아도, 더 이상 필요하지 않은 메모리를 자동으로 식별하고 회수하여 메모리 누수를 방지하고 프로그램 성능을 유지하는 역할을 합니다.
이를 자동으로 하여 개발자의 부담을 줄이고 오류를 방지하기 위해 GC가 도입

  • 작동 방식
    참조되지 않은 객체(Unreachable Object): 더 이상 접근할 수 없는 객체를 탐지하고 메모리를 해제
public class Main {
    public static void main(String[] args) {
        String a = new String("Hello");  // 객체 "Hello" 생성
        String b = a;                   // b가 a를 참조
        a = null;                       // a가 참조를 해제

        // 이제 "Hello" 객체는 b에 의해 여전히 참조됨.
        b = null;                       // b도 참조를 해제

        // "Hello" 객체는 더 이상 참조되지 않음 -> Unreachable 상태
    }
}

힙 영역에서 동작하며 객체가 더 이상 사용되지 않으면 해당 메모리를 회수
: 힙 영역이란 프로그램 실행 중에 동적으로 생성되는 객체들이 저장되는 메모리 공간으로 보통 참조 변수를 통해 접근할 수 있습니다.(Java - new키워드)
<-> 스택 메모리는 함수 호출과 함께 할당 및 해제가 즉시 이루어지기 때문에 GC의 대상이 아니다.

가비지 컬렉션의 동작 단계
1. Mark - 모든 객체를 탐색하면서, 활성 상태(참조되고 있는 객체)인지 확인합니다.
2. Sweep - 활성 상태가 아닌 객체를 해제하여 메모리를 회수합니다.
3. Compaction(이 단꼐는 선택적이다) - 메모리를 정리하고 조각(Fragmentation)을 최소화하고 연속적인 빈 공간을 확보합니다.

GC의 주요 알고리즘
1. 참조 카운팅(Reference Counting) -> 파이썬과 java의 차이점 참고
2. 추적 기반(Tracing)

  • 객체 그래프를 따라가면서 참조되지 않은 객체를 식별
    : 객체 그래프는 객체들 간의 참조 관계를 시각화한 구조
public class Main {
    public static void main(String[] args) {
        Node a = new Node("A");
        Node b = new Node("B");
        Node c = new Node("C");

        a.next = b;  // A -> B
        b.next = c;  // B -> C
        c.next = null; // C -> null (끝)
    }
}

위 코드에서 각 객체 A, B, C는 서로 연결된 참조 그래프를 형성
: 추적 기반 방식에서는 Root 객체에서 시작하여 객체 그래프를 탐색합니다. Root 객체는 현재 프로그램에서 직접적으로 참조되는 객체를 말합니다. ex) 전역 변수, 스택에 저장된 지역 변수, 정적 필드 등이 Root가 됩니다.
-> Mark: Root 객체에서 시작하여 모든 참조를 따라가면서 Reachable 객체(도달 가능한 객체)를 마킹합니다. 도달하지 못한 객체는 자동으로 Unreachable로 간주됩니다. -> Sweep: 마킹되지 않은(Unreachable) 객체를 메모리에서 제거합니다. -> Compaction(선택적): 힙 메모리를 재정렬하여 메모리 단편화(Fragmentation)을 줄이고 연속적인 공간을 확보합니다.
Java에서의
Stop-the-World는 GC가 실행되는 동안 애플리케이션이 멈추는 방식입니다.

  • GC가 힙 메모리를 정리하는 동안 모든 작업이 중단됩니다.
  • 이로 인해 일시적인 성능 저하가 발생할 수 있습니다.
    Generational GC(세대별 가비지 컬렉션)
  • 객체를 생존 기간에 따라 관리하여 효율성을 높입니다.
    • Young Generation : 새로 생성된 객체가 저장되는 공간. 대부분의 객체가 짧은 생명 주기를 가짐.
    • Old Generation : Young Generation에서 살아남은 객체가 이동하는 공간
    • Permanent Generation : 클래스 메타데이터 등 실행 중 고정된 데이터가 저장
      -> Young Generation에서 Mark-Sweep을 수행. -> 생존한 객체를 Old Generation으로 이동 -> Old Generation은 주기적으로 정리
      => 더 이상 사용되지 않는 객체를 정확히 식별 가능하여 메모리 관리가 효율적이고 순환 참조 문제가 발생하지 않는다. 하지만 Stop-the-World 방식에서 프로그램이 멈추는 시간이 발생하고 객체 그래프 탐색에 시간이 소요될 수 있습니다. 또한 Generation GC 등 최적화된 알고리즘이 필요하므로 복잡성이 증가합니다.

즉 가비지 컬렉션의 장단점은
메모리 관리의 자동화로 프로그래머의 부담이 줄어들고 메모리 누수 및 잘못된 해제 오류를 방지할 수 있기 때문에 안정적인 애플리케이션 실행이 가능하다.
하지만 GC 수행 중 Stop-the-World 현상등 애플리케이션 성능이 저하될 수 있고 실시간 응답성이 중요한 프로그램에서는 이로 인해 비효율적일 수 있다. 또한 직접적으로 해제를 할 수 없기에 비효율적인 메모리 해제가 발생할 수 있다.

GC가 발생하는 시점은 JVM이나 런타임 환경에 따라 다르지만 보통
1. 메모리가 부족할 때
2. 프로그램이 여유 상태일 때
3. 명시적으로 System.gc() 호출될 때 일어난다.

효율적인 GC를 위해서는 불필요한 객체 생성을 피해야 하며 사용하지 않는 객체를 명시적으로 null로 설정하는 참조 해제(Nullify)를 사용하고 객체의 생명주기를 고려한 설계를 해야한다. 또한 GC 최적화를 위해 JVM 옵션을 조정할 수 있다.

신입 백엔드 개발자로서 취업 준비 상황에 맞춰, 가비지 컬렉션(GC)과 관련된 실습 중 꼭 필요한 것들만 간추려서 정리해드릴게요!


1. GC 동작 이해와 로그 분석

GC는 백엔드 시스템의 성능에 영향을 주기 때문에, 기본적인 GC 로그 분석 능력은 신입 개발자에게 큰 강점이 될 수 있습니다.

추천 실습

  1. 간단한 Java 프로젝트를 실행하고, GC 로그를 활성화해 동작을 관찰.

    java -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log -jar your-app.jar
  2. 로그에서 주요 항목 확인:

    • GC 발생 시간.
    • GC 수행 시간.
    • 힙 메모리 변화량.
  3. 도구를 사용해 분석:

배우는 점

  • GC가 애플리케이션 성능에 미치는 영향을 직관적으로 이해.
  • 로그를 읽고 GC 관련 문제를 발견하는 방법 학습.

2. 다양한 GC 알고리즘 실험

GC 알고리즘을 이해하면 프로젝트 환경에 맞는 JVM 설정을 제안하거나, 튜닝 방향을 제시할 수 있습니다.

추천 실습

  1. 간단한 REST API 서버(Spring Boot 프로젝트)를 생성.
  2. JVM 옵션을 변경하여 알고리즘별 성능 비교.
    • Serial GC: -XX:+UseSerialGC
    • Parallel GC: -XX:+UseParallelGC
    • G1 GC: -XX:+UseG1GC
  3. 부하 테스트 도구(JMeter 추천)로 트래픽을 생성하고 응답 시간 비교.

배우는 점

  • 상황별로 어떤 GC 알고리즘이 적합한지 이해.
  • GC의 선택이 애플리케이션 성능에 미치는 영향을 실습으로 체험.

3. 메모리 누수 탐지

메모리 누수는 백엔드 시스템에서 종종 발생하는 문제입니다. 이를 해결하는 능력은 면접에서 차별화된 강점이 됩니다.

추천 실습

  1. 의도적인 메모리 누수 코드 작성:

    import java.util.ArrayList;
    import java.util.List;
    
    public class MemoryLeakDemo {
        public static void main(String[] args) {
            List<Object> list = new ArrayList<>();
            while (true) {
                list.add(new Object()); // 메모리를 계속 소비
            }
        }
    }
  2. Heap Dump 생성:

    • JVM 옵션: -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./dump.hprof
  3. Eclipse MAT로 힙 덤프 분석:

    • 누수 원인을 시각적으로 파악.

배우는 점

  • 메모리 누수 문제를 찾고 해결하는 능력 배양.
  • 실제 프로젝트에서 발생할 수 있는 문제에 대비.

4. GC 최적화 적용

면접에서 "GC를 어떻게 튜닝해봤나요?" 같은 질문에 답할 준비를 할 수 있습니다.

추천 실습

  1. JVM 힙 크기 조정 실험:
    • 옵션: -Xms512m -Xmx2g -XX:+UseG1GC.
    • 힙 크기를 작게 설정한 경우와 크게 설정한 경우의 차이점 관찰.
  2. GC 튜닝 전후 응답 시간과 처리량 비교:
    • 부하 테스트를 사용하여 튜닝 효과 확인.

배우는 점

  • JVM 설정 변경이 애플리케이션에 미치는 영향을 이해.
  • 최적화를 위한 힙 크기 조정 방법 학습.

우선순위 요약

  1. GC 로그 분석: 기본 중의 기본, GC 로그를 읽고 성능을 확인할 수 있는 능력.
  2. GC 알고리즘 실험: Serial, Parallel, G1 GC 간의 차이 이해.
  3. 메모리 누수 탐지: 실무에서 자주 발생하는 문제 해결 능력 배양.
  4. GC 최적화: JVM 힙 크기 조정과 성능 차이를 체감.

이 정도만 진행하면 신입 개발자로서 GC에 대한 기본 실무 역량을 충분히 갖출 수 있습니다. 궁금한 점이 있거나 더 필요한 자료가 있다면 말씀해주세요! 😊

0개의 댓글