GC(Garbage Collection, 가비지 컬렉션)는 JVM(Java Virtual Machine)에서 메모리 관리를 자동화하는 메커니즘으로, 더 이상 참조되지 않는 객체를 자동으로 식별하고 메모리에서 제거하여 메모리 누수를 방지한다.

JDK의 버전이 올라가면서 GC(Garbage Collection) 방식은 지속적으로 개선되어 왔다. 새로운 GC 알고리즘이 도입되고 기존의 GC 알고리즘이 최적화되었지만, 기본적인 Major GC와 Minor GC의 매커니즘 자체는 근본적으로 변경되지는 않은 듯 하다.

GC에 대한 기초적인 내용과 Major GC와 Minor GC 매커니즘에 대해서 공부해보자

GC를 알아보기 전에 JVM 메모리 구조에 대해서 간단하게 알아보자 

1. JVM (Java Virtual Machine) 메모리 구조

JVM 메모리는 크게 메소드 영역, , 스택, PC Registers, 네이티브 메소드 스택 영역으로 나누어진다   

메소드 영역 (Method Area)

메소드 영역은 클래스 구조(메타데이터), 메소드, 필드 정보, 메소드의 바이트코드, 그리고 상수 풀(Constant Pool) 등을 저장하는 영역이다. 이 영역은 클래스 로딩 시 필요한 메모리를 할당하며, JVM 시작 시 생성되어 프로그램이 종료될 때까지 유지된다.

힙 (Heap)

힙 영역은 자바 애플리케이션에서 런타임 시 동적으로 생성된 객체들이 주로 저장되는 메모리 영역이다. new 키워드로 생성된 인스턴스나 배열의 객체가 저장된다. 대부분의 GC는 해당 영역에서 발생한다

스택 (Stack)

스택 영역은 각 스레드마다 생성되는 메모리 공간으로, 메소드 호출 시 생성되는 프레임(프레임에는 메소드의 매개변수, 로컬 변수, 중간 계산 결과 등이 저장됨)을 저장한다. 스택은 메소드 호출이 끝나면 프레임이 제거되며, LIFO(Last In, First Out) 방식으로 작동한다. 스택 영역은 스레드별로 독립적이며, 주로 메소드 호출과 관련된 임시 데이터를 저장한다.

PC  레지스터

PC 레지스터는 JVM이 현재 실행 중인 명령어의 주소를 저장하는 작은 메모리 공간이다. 각 스레드는 자신만의 PC 레지스터를 가지고 있으며, 스레드가 어느 명령어를 실행 중인지 추적하는 데 사용된다.

네이티브 메소드 스택 (Native Method Stacks)

자바 외 언어로 작성된 네이티브 코드를 위한 메모리 영역이다

클래스 로더 (Class Loader)

클래스 로더는 자바 클래스 파일을 JVM으로 로딩하고, JVM이 실행할 수 있도록 준비하는 역할을 담당한다. 자바는 런타임 시에 필요한 클래스를 동적으로 로딩하며, 클래스 로더는 이 과정을 처리하는 컴포넌트이다.

클래스 로더의 주요 기능은 다음과 같다

  • 클래스 로딩
    • 클래스 파일(.class)을 JVM 메모리 구조 중 메소드 영역(Method Area)으로 로드한다. 여기서 클래스의 구조, 메소드 정보, 필드 정보, 상수 풀 등이 JVM의 메모리 공간에 저장된다.
  • 클래스 링크
    • 로드된 클래스 파일을 JVM의 메모리 구조에 맞게 링크하는 과정이다. 링크 단계는 세 가지 하위 단계로 나뉜다.
    • 검증
      • 클래스 파일이 JVM의 명세에 맞는지 검증한다. 이 과정에서 클래스 파일의 포맷, 바이트코드의 유효성 등이 확인된다.
    • 준비
      • 클래스의 정적 필드를 메모리에 할당하고 기본값으로 초기화한다
    • 분석
      • 상수 풀(Constant Pool) 내의 심볼릭 레퍼런스를 실제 메모리 주소로 변경합니다.
  • 클래스 초기화
    • 클래스 로딩의 마지막 단계로, 정적 초기화 블록(static block)과 정적 필드를 초기화하는 단계이다.
    • 이 단계에서 클래스가 실제로 실행될 준비가 된다.

Execution Engine

Execution Engine은 JVM에서 로드된 클래스 파일의 바이트코드를 실제로 실행하는 컴포넌트이다. Execution Engine 은 자바 바이트코드를 기계어로 변환하여 CPU에서 실행할 수 있도록 한다.

바이트 코드 해석 시 인터프리터, JIT 컴파일러를 사용하며 각 해석기에 대한 내용은 다음과 같다

인터프리터 (Interpreter)

바이트코드를 직접 해석하여 실행한다. 바이트코드를 한 줄씩 읽어와서 실행하는 방식으로, 메모리 사용이 적지만 반복적인 코드 실행에서는 성능이 떨어질 수 있다.

JIT 컴파일러 (Just-In-Time Compiler)

반복적으로 실행되는 바이트코드를 기계어로 컴파일하여 성능을 향상시키는 역할을 한다

애플리케이션이 시작될 때, 인터프리터가 바이트코드를 즉시 실행하여 초기 실행을 처리하고, JIT 컴파일러는 애플리케이션이 실행되는 동안, 자주 실행되는 코드(핫스팟 코드)를 감지하고 이를 기계어로 컴파일하여 성능을 향상시킨다

클래스 로더는 자바 클래스 파일을 메모리에 로드하고, 이를 Execution Engine이 이해할 수 있는 형태로 준비한다. Execution Engine은 Class Loader에 의해 메모리에 로드된 클래스의 바이트코드를 실제로 실행하여 프로그램의 동작을 구현한다.

JDK 구조 (위키백과 참고)

 

2. Heap 영역의 Young Generation과 Old Generation

Heap 영역은 객체가 할당되는 메모리 영역으로, GC의 주요 대상이다
힙은 크게 Young Generation Old Generation으로 나뉜다.

  • Young Generation: 새로 생성된 객체가 할당되는 영역이다. 이 영역은 다시 Eden, Survivor1, Survivor2로 나뉜다
    대부분의 객체가 Young Generation에서 생성되고 참조되지 않으면 제거된다. Young Generation에 대한 가비지 컬랙션을 Minor GC라고 부른다
    • Eden Space : 새롭게 생성된 객체가 최초 할당되는 영역이다.
    • Survivor Spaces : 두 개의 서바이버 영역(S1, S2)이 있으며, 에덴 영역에서 살아남은 객체가 이 영역으로 이동한다. 이 영역은 번갈아 가며 사용된다
  • Old Generation: Young Generation에서 살아남은 객체가 옮겨지는 영역으로, 오래된 객체가 저장된다
    • Young Generation에서 오래 살아남은 객체가 이동하는 영역이다. 이 영역에 있는 객체들은 수명이 길거나 프로그램이 종료될 때까지 남아 있는다.
    • Old Generation 에 대한 가비지 컬랙션을 Major GC라고 부른다
  • Permanent Generation: 클래스 메타데이터와 같은 JVM의 메타데이터를 저장하는 영역이다. JDK 8부터 Metaspace로 대체되었다.

3. GC의 기본 동작 방식

1) 객체 할당

  • 객체가 생성될 때 JVM은 힙 메모리의 Young Generation 내의 Eden 영역에 객체를 할당한다
  • Eden 영역이 가득 차면 Minor GC가 트리거된다.

2) 마킹 (Marking)

  • GC는 힙 메모리에서 여전히 사용 중인 객체와 그렇지 않은 객체를 식별한다.
  • 이 과정을 "마킹"이라고 하며, 살아있는 객체는 마킹된다.
  • 참조되지 않는 객체는 마킹되지 않으며, 삭제 대상이 된다.

3) 객체 이동 (Minor GC)

  • 살아남은 객체는 Eden 영역에서 Survivor 영역(S1 또는 S2)으로 이동한다.
  • 여러 번의 GC를 통해 Survivor 영역에서 살아남은 객체는 Old Generation으로 승격된다.

4) 객체 삭제 및 메모리 해제

  • 마킹되지 않은, 즉 참조되지 않는 객체는 삭제되어 메모리가 해제된다.
  • Minor GC는 주로 Young Generation에서 동작하며, Major GC는 Old Generation에서 발생한다.

5) 메모리 압축 (Compaction):

  • 객체 삭제 후 남은 메모리에 단편화가 발생할 수 있다. 이 경우 GC는 메모리를 압축하여 단편화를 줄일 수 있다.
  • 메모리 압축은 주로 Major GC나 Full GC의 일부로 수행된다.
메모리 단편화란?
메모리 할당과 해제가 반복되면서, 연속된 메모리 블록 사이에 작은 빈 공간이 생겨나는 경우 총 메모리 공간은 충분하지만 연속적인 큰 메모리 블록을 할당할 수 없는 상황을 의미한다.

이를 해결하기 위해서 사용 중인 메모리를 힙의 한쪽으로 몰아 빈 메모리 블록을 연속된 공간으로 만드는 작업을 수행하는데 이 작업을 메모리 압축이라고 한다

4. GC의 주요 유형

1) Minor GC

  • Young Generation에서 발생한다
  • Eden 영역에서 생성된 대부분의 객체는 수명이 짧기 때문에 빠르게 제거된다.
  • 살아남은 객체는 Survivor 영역으로 이동하며, 일정 횟수의 GC를 거친 객체는 Old Generation으로 승격된다.

2) Major GC:

  • Old Generation에서 발생합니다.
  • 수명이 긴 객체들을 관리하며, Minor GC보다 더 많은 시간이 소요된다.
  • Major GC는 힙 메모리의 많은 부분을 정리하며, 성능에 큰 영향을 미칠 수 있다.

3) Full GC:

  • 전체 힙 메모리(Young + Old Generation)를 대상으로 수행된다.
  • 모든 영역을 동시에 청소하므로, 일시 중지 시간이 길어진다.
  • Full GC가 자주 발생하면 애플리케이션 성능이 크게 저하될 수 있다.

 

 

결론

기존의 Major GC와 Minor GC 개념은 새로운 GC 알고리즘에서도 유지되고 있다. 다만, 새로운 GC 알고리즘들은 이들 GC 과정의 효율성을 높이기 위해 다양한 최적화를 적용했다.

Minor GC는 여전히 Young Generation에서 주로 발생하며, 새로운 객체가 메모리에 할당될 때 주기적으로 발생한다. G1 GC, ZGC, Shenandoah GC 모두 Minor GC 과정을 최적화하여 더 짧은 지연 시간과 효율적인 메모리 관리를 제공한다.

Major GC도 Old Generation의 메모리를 정리하는 과정이지만, 새로운 GC 알고리즘은 이 과정을 병행으로 수행하거나, 더 효율적인 메모리 회수 전략을 사용하여 중단 시간을 최소화하고 있다. 

JDK 버전이 올라가면서 GC 알고리즘은 크게 개선되었으나 기본적인 Major GC와 Minor GC 매커니즘은 유지되고 있으며, 이를 더 효율적으로 수행하기 위해 다양한 기술이 적용되고 있다. 개발자는 애플리케이션의 요구사항에 따라 적절한 GC 알고리즘을 선택하고, JVM 튜닝을 통해 성능을 최적화할 수 있다.

+ Recent posts