관리 메뉴

The Nirsa Way

[Java GC] Old Generation은 어떻게 수집될까: Parallel GC와 Full GC, 할당 성능 본문

Development/JAVA

[Java GC] Old Generation은 어떻게 수집될까: Parallel GC와 Full GC, 할당 성능

KoreaNirsa 2026. 7. 29. 18:17
반응형
[Java GC] Old Generation은 어떻게 수집될까: Parallel GC와 Full GC, 할당 성능

이전 포스팅에서는 Young Generation에서 객체가 어떻게 할당되고, Young GC에서 살아남은 객체가 Survivor Space를 거쳐 Old Generation으로 승격되는 과정을 살펴봤습니다.

이번 글에서는 그다음 단계인 Old Generation을 다룹니다.

Old Generation에는 Young GC에서 여러 번 살아남았거나, Survivor Space에 보관하기 어려워 승격된 객체가 주로 저장됩니다. 이러한 객체는 Young Generation의 객체보다 오래 살아 있을 가능성이 높으므로, Young GC와 같은 복사 방식만으로 설명하기 어렵습니다.

또한 Old Generation의 수집은 객체 할당 속도와 분리된 문제가 아닙니다. 애플리케이션이 객체를 빠르게 생성하면 Young GC의 발생 빈도가 달라지고, 살아남는 객체가 많다면 Old Generation으로 이동하는 객체도 증가할 수 있습니다.

먼저 간단히 정리하자면 아래와 같습니다.

  1. 이 글은 JDK 21의 Parallel GC를 중심으로 Old Generation 수집을 설명합니다.
  2. Parallel GC는 여러 GC 스레드로 Young Generation과 Old Generation을 수집합니다.
  3. Old Generation 수집에서는 생존 객체를 재배치해 메모리 단편화를 줄입니다.
  4. Full GC는 일반적으로 Old Generation을 포함한 전체 힙을 대상으로 수행됩니다.
  5. 높은 할당 속도가 반드시 높은 승격량을 의미하지는 않습니다.
  6. GC 성능은 처리량, 할당 속도, 중단 시간, 전체 처리 시간을 함께 봐야 합니다.

먼저 글을 시작하기 전에 참고로 해당 포스팅은 Parallel GC를 기준으로 설명 합니다.

 

JDK 21의 일반적인 서버 환경에서는 G1 GC가 기본 수집기로 선택됩니다.

G1 GC는 힙을 동일한 크기의 리전으로 나누며, Concurrent Marking과 Mixed GC를 통해 일부 Old Region을 점진적으로 회수합니다. 따라서 이 글에서 다루는 연속된 Young Generation과 Old Generation, Old Generation 압축 과정은 G1 GC의 전체 동작을 설명하는 내용이 아닙니다.

이번 글은 다음 옵션으로 활성화하는 Parallel GC를 기준으로 합니다.

java -XX:+UseParallelGC -jar application.jar

Parallel GC는 처리량 수집기(Throughput Collector)라고도 부르는 세대별 수집기입니다.

Young Generation의 Minor Collection과 Old Generation을 포함하는 Major Collection에 여러 GC 스레드를 사용합니다.

JDK 21 HotSpot JVM
        ↓
-XX:+UseParallelGC
        ↓
Parallel GC
        ├─ Young Generation 병렬 수집
        └─ Old Generation 병렬 수집

Old Generation에는 어떤 객체가 저장될까

이전 글에서 살펴본 것처럼 Young GC에서 살아남은 객체는 다른 Survivor로 복사되거나, 일정한 조건에 따라 Old Generation으로 승격될 수 있습니다.

대표적인 승격 조건은 다음과 같습니다.

  • 객체의 나이가 승격 기준에 도달한 경우
  • Survivor 에 생존 객체를 모두 저장하기 어려운 경우

Old Generation에는 이러한 과정을 거쳐 비교적 오래 살아남은 객체가 주로 모입니다.

Young Generation
        ↓
Young GC에서 생존
        ↓
Survivor에서 계속 생존
        ↓
승격 조건 충족
        ↓
Old Generation

다만 모든 객체가 반드시 Eden과 Survivor Space를 거쳐 Old Generation으로 이동하는 것은 아닙니다.

객체의 크기와 사용 중인 수집기의 정책에 따라 별도의 할당 경로가 사용될 수 있습니다. 예를 들어 G1 GC에서는 리전 크기의 절반 이상인 Humongous Object를 Old Generation에 속하는 Humongous Region에 직접 할당합니다.

이번 포스팅에서 핵심적으로 봐바야 할 부분은 Old Generation에는 Young Generation보다 오래 살아있는 객체가 많을 수 있으며, 이러한 객체를 어떻게 수집하고 공간을 정리할지를 봐야 합니다.

※ 리전(Region)
G1 GC가 Java 힙을 관리하기 위해 나눈 동일한 크기의 메모리 구역입니다. 각 리전은 상황에 따라 Eden, Survivor, Old 등의 용도로 사용됩니다.

※ Humongous Object
크기가 리전 하나의 50% 이상인 매우 큰 객체입니다. 일반 객체처럼 Young Generation의 Eden에 할당하고 복사하기에는 비용이 크므로 별도로 처리합니다.

※ Humongous Region
Humongous Object를 저장하기 위해 사용되는 Old Generation 소속의 리전입니다. 객체가 리전 하나보다 크다면 여러 개의 연속된 리전을 사용합니다.

예를 들어 리전 크기가 4MB라면, 크기가 2MB 이상인 객체는 Humongous Object로 분류되어 Humongous Region에 직접 할당됩니다.

 

 


 

Old Generation은 왜 Young Generation과 다르게 수집할까

Young Generation에는 생성된 지 얼마 되지 않은 객체가 모입니다.

약한 세대 가설에 따라 이러한 객체 중 상당수는 비교적 빠르게 도달 불가능한 상태가 됩니다. 따라서 Young GC에서는 살아 있는 객체만 다른 Survivor Space나 Old Generation으로 복사하고, 기존 Eden과 From Survivor를 한 번에 다시 사용하는 방식이 효율적일 수 있습니다.

반면 Old Generation에는 여러 번의 GC에서 계속 살아남은 객체가 주로 저장됩니다.

# Young Generation
새 객체가 주로 저장됨
        ↓
짧은 수명의 객체가 많을 가능성
        ↓
생존 객체만 복사

# Old Generation
오래 살아남은 객체가 주로 저장됨
        ↓
생존 객체 비율이 높을 가능성
        ↓
생존 객체 재배치와 공간 압축

Old Generation에서 살아 있는 객체가 많다면 모든 생존 객체를 별도의 빈 공간으로 복사하기 위해 큰 추가 공간이 필요할 수 있습니다.

따라서 Parallel GC의 Old Generation 수집에서는 생존 객체를 같은 세대 안에서 재배치하고, 분산된 빈 공간을 하나로 모으는 압축 과정이 중요합니다.

 


 

병렬 컬렉션이란

병렬 컬렉션은 여러 GC 스레드가 하나의 가비지 컬렉션 작업을 나누어 수행하는 방식입니다.

여러 CPU 코어를 사용할 수 있는 환경에서는 한 개의 스레드가 모든 작업을 수행하는 것보다 수집 시간을 줄일 수 있습니다.

JDK 21의 Parallel GC는 여러 GC 스레드를 사용해 수집 작업을 수행하며, CPU가 하나뿐인 환경에서는 병렬 처리의 동기화 비용 때문에 Serial GC보다 불리할 수 있습니다. 반대로 여러 CPU와 중대형 힙을 사용하는 환경에서는 Serial GC보다 높은 처리량을 기대할 수 있습니다.

병렬 수집과 동시 수집은 서로 다른 개념입니다.

구분 의미
병렬(Parallel) 여러 GC 스레드가 수집 작업을 나누어 수행합니다.
동시(Concurrent) GC 작업 일부가 애플리케이션 실행과 겹쳐 수행됩니다.

Parallel GC의 주요 수집 작업은 STW(Stop-The-World) 상태에서 이루어집니다.

애플리케이션 실행
    ↓
STW 시작
    ↓
애플리케이션 스레드 정지
    ↓
여러 GC 스레드가 병렬로 수집
    ↓
STW 종료
    ↓
애플리케이션 실행 재개

즉, Parallel GC에서의 병렬은 애플리케이션과 GC가 동시에 실행된다는 의미가 아니라, 여러 GC 스레드가 함께 수집한다는 의미입니다.

 


 

Parallel Old와 Serial Old는 어떻게 이해해야 할까

첨부된 내용에서는 Old Generation을 수집하는 방식으로 Parallel Old와 Serial Old를 구분합니다.

이 명칭은 HotSpot GC의 역사와 내부 구성을 이해할 때 사용할 수 있습니다.

Parallel Old는 여러 GC 스레드를 사용해 Old Generation을 수집하고 압축하는 방식을 가리킵니다.

Serial Old는 하나의 GC 스레드가 Old Generation을 수집하고 압축하는 방식을 가리킵니다.

다만 JDK 21에서는 두 방식을 독립적인 선택지처럼 이해하면 안 됩니다.

과거에는 Parallel Scavenge + Serial Old 조합을 사용할 수 있었지만, 이 조합은 JDK 14에서 deprecated 되었고 관련 UseParallelOldGC 옵션은 JDK 15에서 obsolete 처리되었습니다. JDK 21에서는 -XX:+UseParallelGC로 Parallel GC 전체를 선택하며 Minor Collection과 Major Collection 모두 병렬로 수행됩니다.

따라서 JDK 21에서는 다음과 같이 이해하는 것이 적절합니다.

구분 Serial GC Parallel GC
활성화 옵션 -XX:+UseSerialGC -XX:+UseParallelGC
Young Generation 수집 단일 GC 스레드 여러 GC 스레드
Old Generation 수집 단일 GC 스레드 여러 GC 스레드
주요 목적 작은 힙과 제한된 CPU 환경 다중 코어 환경의 높은 처리량
STW 발생합니다. 발생합니다.
Old Generation 압축 수행합니다. 수행합니다.

Parallel Old는 JDK 21에서 별도로 선택하는 GC 이름이라기보다, Parallel GC의 Old Generation 수집 부분을 설명하는 명칭으로 보는 편이 정확합니다.

 


 

Old Generation에서는 왜 압축이 필요할까

Old Generation에서 도달 불가능한 객체가 사용하던 공간만 비우면 메모리 곳곳에 작은 빈 공간이 남을 수 있습니다.

// 수집 전
[객체 A][가비지][객체 B][가비지][객체 C]

가비지 객체가 사용하던 공간을 회수하면 다음과 같이 됩니다.

// 공간 회수 후
[객체 A][빈 공간][객체 B][빈 공간][객체 C]

전체 빈 공간의 합은 충분하더라도 하나의 큰 객체를 저장할 연속된 공간이 부족할 수 있습니다.

  1. 빈 공간 2MB + 빈 공간 3MB + 빈 공간 5MB
  2. 새 객체 크기 6MB

위와 같은 상황일 때 6MB를 가진 공간이 없기 때문에 즉시 할당이 어려워집니다.

이처럼 사용 가능한 공간이 여러 위치에 흩어져 큰 연속 공간을 확보하기 어려운 상태를 외부 단편화(External Fragmentation)라고 합니다.

Parallel GC의 Old Generation 수집에서는 살아 있는 객체를 재배치하여 빈 공간을 통합합니다.

// 압축 전
[객체 A][빈 공간][객체 B][빈 공간][객체 C]

// 압축 후
[객체 A][객체 B][객체 C][ 연속된 빈 공간 ]

압축 과정은 다음과 같이 이해할 수 있습니다.

객체가 이동하면 해당 객체를 가리키는 참조도 새로운 위치를 가리키도록 수정해야 합니다.

따라서 압축은 메모리 단편화를 줄이는 장점이 있지만, 생존 객체를 이동하고 참조를 갱신하는 비용이 발생합니다.

Parallel GC의 Old Generation 수집 알고리즘은 구현 관점에서 Mark-Summary-Compact 계열로 설명됩니다. 반드시 Mark → Sweep → Compact라는 세 개의 독립 단계가 실행된다고 이해하기보다 아래와 같은 흐름으로 이해하는 편이 좋습니다.

  1. 생존 객체 식별
  2. 생존 객체 재배치
  3. 분산된 빈 공간 통합

 


 

Full GC란 무엇일까

Full GC는 일반적으로 Young Generation만이 아니라 Old Generation을 포함한 전체 Java Heap을 대상으로 수행되는 수집을 의미합니다.

Parallel GC에서 Full GC가 발생하면 애플리케이션 스레드를 멈춘 상태에서 전체 힙의 생존 객체를 처리하고, Old Generation의 객체를 재배치해 공간을 압축합니다.

Full GC는 Young GC보다 일반적으로 처리 범위가 넓으며 Old Generation에는 살아 있는 객체가 많이 존재할 수 있으므로 객체 이동과 참조 갱신 비용도 커질 수 있습니다.

구분 Young GC Full GC
주요 수집 대상 Young Generation 전체 Java Heap
Old Generation 처리 일반적인 주 대상이 아님 포함
주요 작업 생존 객체 복사와 승격 전체 힙 처리와 Old Generation 압축
STW Parallel GC에서는 발생 발생
일반적인 비용 상대적으로 작음 상대적으로 큼

 

Major GC와 Full GC

Major GC는 문서나 분석 도구에 따라 의미가 다르게 사용될 수 있습니다.

일부 자료에서는 Old Generation을 수집하는 작업을 Major GC라고 부르고, 다른 자료에서는 Full GC와 유사한 의미로 사용합니다.

  • Minor GC : 일반적으로 Young Generation 수집
  • Major GC : 일반적으로 Old Generation의 수집을 의미하지만 구현체나 자료에 따라 Old Generation만 수집하는 경우 또는 전체 힙을 수집하는 경우를 가리킬 수 있음
  • Full GC : 일반적으로 전체 힙을 대상으로 수집

Oracle의 JDK 21 Parallel GC 문서는 Minor Collection과 Major Collection이 모두 병렬로 수행된다고 설명합니다. 실제 운영에서는 Major GC라는 이름만으로 수집 범위를 단정하지 말고 GC 로그에 표시된 이벤트와 수집기를 함께 확인해야 합니다.

 


 

Full GC는 언제 발생할까

Full GC는 Old Generation이 정확히 100% 찼을 때만 발생하는 작업이 아닙니다.

Parallel GC에서는 다음과 같은 상황과 연결될 수 있습니다.

 

1. 승격할 공간을 확보하지 못한 경우

Young GC에서 살아남은 객체를 Old Generation으로 승격해야 하지만 충분한 공간을 확보하지 못하면 Full GC가 수행될 수 있습니다.

 

2. 객체 할당에 필요한 공간이 부족한 경우

Young Generation과 Old Generation에서 필요한 공간을 확보하지 못하면 JVM은 Full GC를 통해 공간을 회수하려고 시도할 수 있습니다.

 

3. 명시적인 GC가 요청된 경우

애플리케이션에서 System.gc()를 호출하면 JVM에 GC 수행을 요청합니다.

System.gc();

System.gc()는 명세상 GC 실행을 보장하는 명령이 아니라 요청입니다. 하지만 일반적인 HotSpot JVM에서는 Full GC의 원인이 될 수 있으므로 애플리케이션 코드에서 직접 호출하는 것은 피하는 편이 좋습니다.

 

4. Metaspace 사용량이 높아진 경우

클래스 메타데이터가 저장되는 Metaspace의 사용량이 기준에 도달하면 클래스 언로딩과 메타데이터 회수를 위해 GC가 유도될 수 있습니다.

다만 Metaspace는 Java Heap이 아니라 Native Memory에 존재합니다. 따라서 Full GC를 단순히 “Java Heap 공간이 부족할 때만 발생하는 작업”으로 이해하면 안 됩니다.

 


 

Full GC가 발생하면 무조건 문제일까

Full GC가 한 번 발생했다는 사실만으로 애플리케이션에 문제가 있다고 단정할 수는 없습니다.

플리케이션이 장시간 실행되는 동안 드물게 발생하고, 중단 시간이 서비스 요구사항 안에 있다면 반드시 비정상이라고 볼 수 없습니다.

다만 다음과 같은 상황은 확인해야 합니다.

  • Full GC가 짧은 간격으로 반복됨
  • FUll GC 중단 시간이 계속 증가함
  • Full GC 이후에도 Gold Generation 사용량이 거의 줄지 않음
  • Old Generation 사용량이 지속적으로 증가함
  • GC에 대부분의 실행 시간을 사용함
  • Full GC 이후 OutOfMemoryError가 발생함

특히 Full GC 이후에도 Old Generation의 사용량이 크게 줄지 않는다면 살아 있는 객체의 총량인 Live Set이 크거나, 의도하지 않은 참조가 객체를 계속 유지하는 메모리 누수를 의심할 수 있습니다.

Full GC 이전 Old Generation 90%
        ↓ Full GC
Full GC 이후 Old Generation 85%
        ↓
대부분의 객체가 여전히 생존
        ↓
높은 Live Set 또는 메모리 누수 가능성 확인

Full GC를 분석할 때는 발생 횟수만 볼 것이 아니라 다음 항목을 함께 확인해야 합니다.

  • Full GC 발생 원인
  • Full GC 전후의 힙 사용량
  • Full GC 중단 시간
  • Full GC 발생 간격
  • Old Generation 증가 속도
  • 객체 할당 속도
  • 객체 생존율

 


 

객체 할당 속도와 GC는 어떻게 연결될까

이전 글에서는 TLAB과 범프 포인터 할당을 통해 객체가 빠르게 만들어지는 구조를 살펴봤습니다. 이번 글에서는 할당의 세부 방식보다 객체가 생성되는 속도와 GC 사이의 관계를 작성하려 합니다.

애플리케이션이 객체를 빠르게 생성하면 Young Generation의 여유 공간도 빠르게 줄어듭니다.

첨부 범위의 ModelAllocator 예시는 여러 스레드가 각자의 할당 영역에서 객체를 만들고, 할당자가 새로운 객체 생성을 조정하는 구조를 보여 줍니다.

이 예시의 핵심은 특정 구현 코드 자체보다 다음 관계에 있습니다.

애플리케이션 스레드
        ↓
객체 할당 요청
        ↓
할당 가능한 공간 소비
        ↓
할당 공간 부족
        ↓
GC를 통한 공간 회수

즉, 객체를 만드는 과정과 사용하지 않는 객체의 메모리를 회수하는 과정은 서로 분리되어 있지 않습니다.

 


 

할당 속도가 높으면 Old Generation도 빠르게 찰까

반드시 그렇지는 않습니다.

할당 속도가 높아도 생성된 객체 대부분이 Young GC 전에 도달 불가능한 상태가 된다면 Old Generation으로 승격되는 객체는 많지 않을 수 있습니다.

높은 할당 속도
        +
낮은 객체 생존율
        ↓
Young GC는 자주 발생할 수 있음
        ↓
Old Generation 승격량은 적을 수 있음

반대로 할당 속도와 생존율이 모두 높다면 Survivor Space에 복사되는 객체와 Old Generation으로 승격되는 객체가 증가할 수 있습니다.

높은 할당 속도
        +
높은 객체 생존율
        ↓
Young GC 빈도 증가 가능
        ↓
Survivor 복사량 증가
        ↓
Old Generation 승격량 증가
        ↓
Full GC 압력 증가 가능

개념적인 관계는 다음처럼 표현할 수 있습니다.

Old Generation 승격 압력 ≈ 객체 할당 속도 × Young GC 생존율

이는 JVM에서 사용하는 실제 계산 공식은 아닙니다.

할당 속도만으로 Old Generation의 증가량을 판단해서는 안 되며, Young GC에서 살아남는 객체의 비율도 함께 확인해야 한다는 의미입니다.

 


 

Parallel GC의 승격 버퍼와 단편화

Parallel GC에서는 Young GC에 여러 GC 스레드가 참여합니다.

각 GC 스레드는 생존 객체를 Old Generation으로 승격하기 위해 Old Generation의 일부 공간을 예약할 수 있습니다. Oracle의 JDK 21 문서는 이 공간을 promotion buffer로 설명하며, 여러 GC 스레드가 공간을 나누어 예약하는 과정에서 단편화 효과가 발생할 수 있다고 설명합니다.

Old Generation

[GC 스레드 1의 승격 버퍼]
[GC 스레드 2의 승격 버퍼]
[GC 스레드 3의 승격 버퍼]
[남은 공간]

따라서 Parallel GC에서는 Old Generation 단편화를 단순히 “가비지 객체를 제거한 뒤 빈 공간이 흩어지는 현상”만으로 볼 수 없습니다.

병렬 승격을 위해 여러 GC 스레드가 공간을 나누어 사용하는 과정도 Old Generation의 공간 활용에 영향을 줄 수 있습니다.

Oracle 문서는 이러한 단편화 효과를 줄이는 방법으로 GC 스레드 수를 줄이거나 Old Generation의 크기를 늘리는 방향을 언급합니다. 하지만 실제 환경에서는 GC 스레드 수를 임의로 줄이기보다 GC 로그와 부하 테스트를 통해 전체 처리량과 중단 시간을 함께 확인해야 합니다.

 


 

GC 성능은 네 가지 지표를 함께 봐야 합니다

첨부된 내용에서는 GC 성능을 다음 네 가지 요소로 설명합니다.

성능 요소 살펴보는 관점
처리량 일정 시간 동안 애플리케이션이 수행한 작업량
할당 속도 일정 시간 동안 객체에 할당한 메모리의 양
중단 시간 GC로 애플리케이션 스레드가 멈춘 시간
전체 처리 시간 애플리케이션의 작업이 완료될 때까지 걸린 시간

이 네 가지는 서로 독립적으로 움직이지 않습니다.

한 가지 지표를 개선하기 위한 설정이 다른 지표에는 부정적인 영향을 줄 수 있습니다.

 

 

1. 처리량

처리량은 전체 실행 시간 중 애플리케이션 작업에 사용된 시간의 비율로 표현할 수 있습니다.

처리량
=
애플리케이션 실행 시간
────────────────────────────
애플리케이션 실행 시간 + GC 시간

예를 들어 애플리케이션이 99초 동안 작업하고 GC로 1초 동안 중단되었다면 처리량은 약 99%입니다.

애플리케이션 실행 시간: 99초
GC 시간: 1초
전체 실행 시간: 100초

처리량
= 99 / 100
= 99%

Parallel GC에서는 -XX:GCTimeRatio 옵션으로 처리량 목표를 지정할 수 있습니다.

-XX:GCTimeRatio=19

GCTimeRatio=19는 전체 시간의 약 5%를 GC에 사용하는 목표를 의미합니다.

Parallel GC의 기본값은 99이며, 전체 시간의 약 1%를 GC에 사용하는 것을 목표로 합니다. 이는 반드시 달성되는 보장이 아니라 JVM이 달성하려고 시도하는 목표입니다.

 

2. 할당 속도

할당 속도는 일정 시간 동안 애플리케이션이 객체에 할당한 메모리의 양을 의미합니다.

MB/s
GB/s
objects/s

할당 속도가 높으면 Young Generation이 빠르게 소모되어 Young GC가 자주 발생할 수 있습니다.

하지만 Old Generation의 증가 속도를 판단하려면 객체 생존율과 승격량도 함께 확인해야 합니다.

Allocation Rate
        ↓
Young GC 빈도에 영향

Survival Rate
        ↓
Survivor 복사량과 승격량에 영향

Promotion Rate
        ↓
Old Generation 증가 속도에 영향

 

3. 중단 시간

중단 시간은 GC로 인해 애플리케이션 스레드가 실행되지 못하는 시간입니다.

Parallel GC는 주요 수집 작업을 STW 상태에서 수행하므로 GC 중단 시간이 사용자 응답 시간에 영향을 줄 수 있습니다.

다만 300ms의 GC 중단이 발생했다고 해서 모든 요청의 응답 시간이 정확히 300ms씩 증가하는 것은 아닙니다.

요청의 처리 구간이 STW 구간과 얼마나 겹치는지에 따라 실제 지연은 달라집니다.

운영 환경에서는 평균값뿐 아니라 최대값과 백분위 지표도 함께 확인하는 것이 좋습니다.

평균 GC Pause Time
최대 GC Pause Time
p95 GC Pause Time
p99 GC Pause Time

p95와 p99는 JVM이 직접 사용하는 GC 목표값이라기보다, GC 로그나 JFR에서 수집한 중단 시간을 모니터링 시스템에서 집계한 지표입니다.

 

4. 전체 처리 시간

전체 처리 시간은 하나의 작업을 시작한 시점부터 완료할 때까지 걸린 시간을 의미합니다.

웹 서비스에서는 개별 요청의 지연 시간이 중요할 수 있지만, 배치 애플리케이션에서는 각 GC의 중단 시간보다 전체 작업 완료 시간이 더 중요할 수 있습니다.

설정 최대 GC 중단 시간 전체 작업 시간
설정 A 100ms 15분
설정 B 500ms 10분

실시간 응답이 중요한 서비스라면 설정 A가 더 적합할 수 있습니다.

반면 야간 배치 작업이라면 개별 중단이 길더라도 전체 작업을 더 빠르게 끝내는 설정 B가 적합할 수 있습니다.

 


 

Parallel GC가 사용하는 성능 목표

애플리케이션 관점에서는 처리량, 할당 속도, 중단 시간, 전체 처리 시간을 함께 볼 수 있습니다.

이와 별도로 Parallel GC의 적응형 정책은 다음 세 가지 목표를 사용합니다.

1. 최대 중단 시간
        ↓
2. 처리량
        ↓
3. 최소 힙 Footprint

Parallel GC는 최대 중단 시간 목표를 먼저 고려하고, 해당 목표를 만족한 뒤 처리량 목표를 확인합니다. 두 목표를 모두 만족한 경우 힙 Footprint를 줄이는 방향을 고려합니다.

※ Footprint : 자바 애플리케이션이 실행되는 동안 OS로부터 할당받아 사용하는 총 물리 메모리 양
Parallel GC 목표 관련 옵션
최대 중단 시간 -XX:MaxGCPauseMillis
처리량 -XX:GCTimeRatio
최대 힙 크기 -Xmx

MaxGCPauseMillis는 반드시 지켜지는 제한이 아니라 목표에 가까운 힌트입니다.

중단 시간을 줄이기 위해 세대 크기를 조정하면 전체 처리량이 감소할 수도 있습니다.

 


 

힙을 크게 설정하면 성능이 좋아질까

힙이 커지면 객체를 할당할 수 있는 공간이 늘어나므로 GC 발생 간격이 길어질 수 있습니다.

힙 크기 증가
        ↓
할당 가능한 공간 증가
        ↓
GC 발생 빈도 감소 가능
        ↓
처리량 증가 가능

하지만 힙이 커졌다고 모든 성능 지표가 좋아지는 것은 아닙니다.

Parallel GC에서 Full GC가 발생하면 더 넓은 메모리 영역과 더 많은 생존 객체를 처리해야 할 수 있으므로 한 번의 중단 시간이 길어질 가능성이 있습니다.

반대로 힙을 지나치게 작게 설정하면 GC가 자주 발생하고 애플리케이션이 실제 작업보다 GC에 더 많은 시간을 사용할 수 있습니다.

// 큰 힙
장점
→ GC 발생 간격 증가 가능
→ 처리량 증가 가능

주의점
→ Full GC 처리량 증가 가능
→ 긴 STW 가능
→ 메모리 사용량 증가


//작은 힙
장점
→ 메모리 Footprint 감소

주의점
→ GC 빈도 증가
→ 승격 공간 부족 가능
→ 처리량 감소 가능

JDK 21의 Parallel GC는 전체 실행 시간의 98% 이상을 GC에 사용하면서 힙의 2% 미만만 회수하는 상태가 계속되면 OutOfMemoryError: GC overhead limit exceeded를 발생시킬 수 있습니다.

 


 

GC 스레드는 많을수록 좋을까

Parallel GC에서 사용하는 병렬 GC 스레드 수는 다음 옵션으로 조정할 수 있습니다.

-XX:ParallelGCThreads=N

GC 스레드를 늘리면 수집 작업을 더 많이 분할할 수 있지만 항상 성능이 좋아지는 것은 아닙니다.

// GC 스레드 증가
기대 효과
→ GC 병렬 처리량 증가 가능
→ GC 중단 시간 감소 가능

주의점
→ 동기화 비용 증가
→ CPU 경쟁 증가
→ 애플리케이션이 사용할 CPU 감소
→ 승격 버퍼 단편화 증가 가능

JVM은 사용 가능한 하드웨어 스레드를 기준으로 GC 스레드 수를 결정합니다. 단일 CPU 환경에서는 병렬 처리 오버헤드 때문에 Serial GC보다 불리할 수 있습니다.

따라서 GC 스레드 수를 임의로 늘리기보다 실제 GC 로그와 부하 테스트를 바탕으로 조정해야 합니다.

 


 

Parallel GC는 어떤 애플리케이션에 적합할까

Parallel GC는 긴 중단 시간을 어느 정도 허용할 수 있고, 일정 시간 동안 최대한 많은 작업을 처리하는 것이 중요한 환경에서 고려할 수 있습니다.

대표적인 예는 다음과 같습니다.

  • 배치 처리
  • 데이터 분석
  • 파일 변환
  • 과학 계산
  • 비동기 작업 처리
  • 오프라인 데이터 처리

Oracle의 JDK 21 가이드는 최대 처리량이 최우선이며 1초 이상의 GC 중단을 허용할 수 있다면 Parallel GC를 선택 기준 중 하나로 고려할 수 있다고 설명합니다.

반면 사용자 요청의 지연 시간이 중요한 웹 애플리케이션이나 API 서버에서는 평균 처리량뿐 아니라 최대 GC 중단 시간과 응답 시간의 p95·p99를 함께 확인해야 합니다.

확인 항목 확인할 질문
CPU 코어 수 병렬 GC에 사용할 CPU가 충분한가
힙 크기 Full GC에서 처리할 메모리 범위가 얼마나 큰가
Live Set GC 이후에도 살아남는 객체의 크기가 얼마나 되는가
할당 속도 Young Generation이 얼마나 빠르게 소모되는가
생존율 Young GC에서 얼마나 많은 객체가 살아남는가
승격 속도 Old Generation이 얼마나 빠르게 증가하는가
허용 중단 시간 서비스가 허용할 수 있는 최대 지연은 얼마인가
전체 처리 시간 작업 완료 시간이 중요한가

 


 

객체 할당부터 Full GC까지의 전체 흐름

지금까지의 내용을 하나로 연결하면 다음과 같습니다.

 

\성능 관점에서는 다음 흐름으로 연결됩니다.

객체 할당 속도
        ↓
Young Generation 소모 속도
        ↓
Young GC 발생 빈도
        ↓
객체 생존율
        ↓
Survivor 복사량과 승격량
        ↓
Old Generation 사용량
        ↓
Full GC 발생 가능성
        ↓
중단 시간과 전체 처리량 변화

 


 

자주 혼동하는 부분
1. JDK 21은 Parallel GC를 기본으로 사용할까

아닙니다.

JDK 21의 일반적인 서버 환경에서는 G1 GC가 기본으로 선택됩니다. Parallel GC를 사용하려면 -XX:+UseParallelGC 옵션을 지정해야 합니다.

 

2. Parallel Old는 별도로 선택하는 GC일까

JDK 21에서는 그렇지 않습니다.

Parallel Old는 Parallel GC의 Old Generation 수집 부분을 설명하는 역사적·구현적 명칭입니다. JDK 21에서는 -XX:+UseParallelGC로 Parallel GC 전체를 선택합니다.

 

3. 병렬 수집은 애플리케이션과 동시에 실행될까

반드시 그렇지는 않습니다.

Parallel GC는 애플리케이션 스레드를 멈춘 뒤 여러 GC 스레드가 수집 작업을 병렬로 수행합니다.

 

4. Old Generation은 오래된 객체를 의미할까

정확히는 객체의 생성 시각이 오래되었다는 뜻보다, GC에서 여러 번 살아남거나 승격 조건을 만족한 객체가 저장되는 세대를 의미합니다.

 

4. 높은 할당 속도는 반드시 Old Generation을 빠르게 채울까

아닙니다.

높은 할당 속도는 Young GC 빈도를 높일 수 있지만, 생성된 객체가 대부분 빠르게 사라진다면 Old Generation 승격량은 적을 수 있습니다.

 

5. Full GC는 Old Generation만 수집할까

일반적으로 그렇지 않습니다.

Full GC는 Young Generation과 Old Generation을 포함한 전체 Java Heap을 대상으로 하는 수집으로 이해하는 것이 적절합니다.

 

6. Full GC가 한 번 발생하면 메모리 누수일까

아닙니다.

Full GC의 발생 간격, 중단 시간, GC 전후의 힙 사용량을 함께 확인해야 합니다. Full GC 이후에도 Old Generation 사용량이 거의 줄지 않고 계속 증가한다면 높은 Live Set이나 메모리 누수를 점검해야 합니다.

 


 

핵심 정리
  • 이 글은 JDK 21의 Parallel GC를 기준으로 Old Generation 수집을 설명했습니다.
  • JDK 21의 일반적인 서버 환경에서는 G1 GC가 기본으로 선택됩니다.
  • Parallel GC는 -XX:+UseParallelGC로 활성화합니다.
  • Parallel GC는 Young Generation과 Old Generation의 수집에 여러 GC 스레드를 사용합니다.
  • Parallel GC의 주요 수집 작업은 STW 상태에서 수행됩니다.
  • Parallel Old는 JDK 21에서 별도로 선택하는 GC가 아니라 Parallel GC의 Old Generation 수집 부분을 가리키는 명칭입니다.
  • Old Generation에는 Young GC에서 살아남아 승격된 객체가 주로 저장됩니다.
  • Old Generation 수집에서는 생존 객체를 재배치하여 메모리 단편화를 줄입니다.
  • 압축 과정에서는 객체 이동과 참조 갱신 비용이 발생합니다.
  • Full GC는 일반적으로 Young Generation과 Old Generation을 포함한 전체 힙을 대상으로 수행됩니다.
  • Full GC는 처리 범위가 넓고 생존 객체가 많을 수 있어 중단 시간이 길어질 가능성이 있습니다.
  • 높은 객체 할당 속도는 Young GC의 빈도를 높일 수 있습니다.
  • Old Generation 승격량은 할당 속도뿐 아니라 객체 생존율에도 영향을 받습니다.
  • Parallel GC에서는 병렬 승격을 위한 promotion buffer가 Old Generation 단편화에 영향을 줄 수 있습니다.
  • GC 성능은 처리량, 할당 속도, 중단 시간, 전체 처리 시간을 함께 확인해야 합니다.
  • Parallel GC의 적응형 정책은 최대 중단 시간, 처리량, 힙 Footprint 순으로 목표를 고려합니다.
  • GC 횟수가 적거나 한 번의 수집 시간이 짧다는 이유만으로 좋은 설정이라고 판단할 수 없습니다.

 


 

마무리

Young Generation에서 객체를 빠르게 할당하고 수집하는 과정은 Old Generation과 분리되어 있지 않습니다.

객체가 계속 생성되면 Young Generation이 소모되고 Young GC가 발생합니다. Young GC에서 살아남은 객체는 Survivor Space로 복사되거나 Old Generation으로 승격됩니다.

할당 속도와 객체 생존율이 모두 높다면 Old Generation의 사용량도 빠르게 증가할 수 있습니다.

Old Generation에 객체를 승격하거나 새로운 객체를 할당할 공간을 확보하기 어려워지면 Full GC가 수행될 수 있습니다.

객체 생성
    ↓
Young Generation 사용량 증가
    ↓
Young GC
    ↓
생존 객체 복사 또는 승격
    ↓
Old Generation 사용량 증가
    ↓
Full GC 가능성 증가

Parallel GC는 이러한 수집 작업에 여러 CPU 코어를 활용하여 전체 처리량을 높이는 것을 목표로 합니다.

그러나 높은 처리량이 짧은 중단 시간을 의미하는 것은 아닙니다. 특히 Full GC에서는 전체 힙과 많은 생존 객체를 처리해야 할 수 있으므로 긴 STW가 발생할 수 있습니다.

따라서 Parallel GC를 평가할 때는 단순히 수집 속도만 비교하지 않고 애플리케이션에서 중요한게 무엇인지 아래 질문을 함께 살펴봐야 합니다.

  • 일정 시간 동안 더 많은 작업을 처리하는가
  • 사용자 응답 지연을 짧게 유지하는가
  • Old Generation의 증가 속도를 낮추는가
  • Full GC 중단 시간을 줄여야 하는가
  • 전체 작업을 더 빠르게 완료해야 하는가

이 기준이 정해져야 처리량, 할당 속도, 생존율, 승격량, 중단 시간과 힙 크기를 어떤 관점에서 측정할지도 분명해집니다.

반응형