1. 배경
Java를 사용한다는 것은 결국 JVM 위에서 동작하는 애플리케이션을 만든다는 의미이다
그리고 JVM을 사용하는 이상, GC는 필수적으로 마주하게 되는 요소다
하지만 대부분의 경우 GC는 "JVM이 알아서 해주는 동작" 정도로만 인식되는 경우가 많다
실제로 서비스 개발 과정에서도 GC를 직접 다룬다는 느낌은 적고,
단순히 Heap Size를 조정하거나 GC 옵션을 일부 변경하는 수준에서 대응하는 경우가 대부분이다
여기서 문제는, GC가 단순한 내부 기능으로 치부하기엔 너무 큰 기능이라는 것이다
GC는 애플리케이션의 응답 시간과 처리량, 그리고 안정성에 직접적인 영향을 준다
GC가 수행되는 동안 애플리케이션이 일시적으로 멈추는 STW 현상은 서비스에서 지연을 유발하며,
GC가 정상적으로 메모리를 회수하지 못하는 경우에는 결국 OOM(Out-Of-Memory) 상황으로 이어지게 된다
이렇듯, GC는 서비스의 성능과 장애에 직접적으로 연결되는 핵심 시스템 요소라고 볼 수 있다
그렇다면 GC는 왜 이런 문제를 가지게 되었고, 이를 해결하기 위해 어떻게 발전해왔을까?
이 글에서는 GC의 발전 과정을 따라가며 각 GC의 특징을 살펴보고, OOM 상황과 대응 방법까지 정리해보고자 한다
2. GC의 변천사
GC는 단순히 새로운 알고리즘이 등장하며 발전한 것이 아니라,이전 방식이 가진 한계를 해결하는 과정 속에서 진화해왔다
초기 GC는 구현이 단순한 대신 STW 시간이 길었고,서비스 규모가 커질수록 이러한 문제는 더욱 두드러지게 나타났다
결국 GC의 발전은 다음과 같은 방향으로 이루어졌다
- 처리 속도를 높이기 위한 방향 (Throughput 개선)
- 애플리케이션 중단 시간을 줄이기 위한 방향 (Latency 개선)
- 예측 가능한 성능을 제공하기 위한 방향
이러한 흐름 속에서 GC는 Serial → Parallel → CMS → G1으로 발전하게 된다
- Serial GC
Serial GC는 가장 기본적인 GC 방식으로, 단일 스레드를 사용하여 GC 작업을 수행한다
GC가 수행되는 동안에는 애플리케이션의 모든 작업이 중단되며, 이러한 STW 현상은 GC가 완료될 때까지 지속된다
이 방식은 구현이 단순하고, 오버헤드가 적기 때문에 Heap 크기가 작고 단일 스레드 환경에서는 효율적으로 동작할 수 있다
하지만 Heap 크기가 커지고 애플리케이션의 규모가 커질수록 GC에 소요되는 시간이 길어진다
-XX:+UseSerialGC 옵션을 통해 활성화할 수 있다 - Parallel GC ─ Java 8 표준
Parallel GC는 Serial GC의 가장 큰 문제였던 느린 GC 수행 시간을 개선하기 위해 등장했다
기존과 동일하게 STW 방식은 유지하지만, GC 작업을 멀티 스레드로 수행하면서 전체 처리 시간을 단축한다
이를 통해 애플리케이션은 전체 처리량(Throughput)을 높일 수 있다
하지만 이는 Pause Time 자체를 줄인 것이 아니라, GC 작업을 빠르게 끝내는 방식으로 개선한 것이며,
여전히 GC가 수행되는 동안 애플리케이션은 중단되고 대규모 서비스에는 여전히 Pause가 문제로 남게 된다
즉, STW는 여전히 존재하며, 지연에 영향을 크게 받는 서비스에는 부적합하다
-XX:+UseParallelGC 옵션을 통해 활성화할 수 있다 - CMS(Concurrent Mark Sweep) GC
CMS GC는 애플리케이션의 중단 시간을 줄이기 위해 등장한 GC이다
기존 GC와 달리, 일부 작업을 애플리케이션 실행과 동시에 수행하여 STW를 최소화하는것을 목표로 한다
이를 통해 응답 지연(Latency)을 줄일 수 있으며, 실시간성이 중요한 서비스에서 효과를 볼 수 있다
하지만 CMS GC는 또 다른 문제를 발생시켰다
GC 작업을 완전한 중단 없이 수행하지는 못하기 때문에 여전히 STW 구간이 일부 존재하며,
Compaction 과정이 없기 때문에 메모리 단편화 문제가 발생하게 된다
이로 인해 충분한 메모리가 있음에도 불구하고, OOM이 발생하는 상황이 생길 수 있다
결국 CMS GC는 지연을 줄이는 데에는 성공했지만 안정성과 예측 가능성 측면에서 한계를 가지게 되었다
현재는 Deprecated 되었으며, G1 GC로 대체되었다 - G1(Garbage First) GC ─ Java 11 표준
G1 GC는 기존 GC들이 가지던 문제를 종합적으로 해결하기 위해 등장한 GC이다
(E: Eden, S: Survivor, O: Old, H: Humongous)
기존의 Heap 구조(Young/Old 영역)를 유지하면서도,
Heap을 여러 개의 Region으로 나누어 관리하는 방식을 사용한다
이러한 구조를 통해 G1 GC는 전체 Heap이 아닌 일부 Region만 선택적으로 GC를 수행하게 되며,
Pause Time 목표를 설정해 STW 시간을 예측할 수 있도록 설계하였다 ( -XX:MaxGCPauseMillis )
또한 CMS에서 발생하던 단편화를 Compaction을 통해 해결하였다
특히 G1 GC는 Young GC와 Mixed GC를 통해 메모리를 효율적으로 회수한다
G1 GC의 핵심은 예측 가능한 GC 동작이다
이전 GC들은 성능이 상황에 따라 크게 달라졌지만,
G1 GC는 사용자가 설정한 목표 시간 내에서 GC를 수행하려 시도한다
이를 통해 대규모 Heap 환경에서도 비교적 안정적인 성능을 제공한다
G1 GC의 내부 동작은 별도의 글에서 자세히 다루도록 하겠다
3. OOM의 발생 원인과 대응
OOM은 하나의 원인으로 발생하기보다는, 여러 조건이 겹쳐서 발생하는 경우가 대부분이다
대표적인 원인들은 다음과 같다
- 메모리 누수
더 이상 사용되지 않는 객체가 참조를 유지한 채 해제되지 않는 경우이다
이러한 경우 GC는 객체를 사용 중으로 판단하기 때문에 메모리를 회수하지 못하게 된다 - 과도한 객체 생성
짧은 시간 동안 대량의 객체가 생성되는 경우이다
대량의 요청 처리나, 스트림/파싱 과정에서 객체를 반복적으로 생성하는 경우 등이 이에 해당하며,
이 경우 GC가 객체 생성 속도를 따라가지 못하면 Heap이 빠르게 소진되면서 OOM으로 이어질 수 있다 - Heap 사이즈 설정 부족
애플리케이션이 사용하는 메모리에 비해 Heap 크기가 부족한 경우이다
이 경우는 비교적 단순하지만, 모든 문제를 Heap을 늘리는 것만으로 해결하려고 하는 것은 미봉책에 불과하다 - GC 동작 지연 또는 비효율
GC가 정상적으로 동작하고 있음에도 불구하고 메모리 회수가 충분히 이루어지지 않는 경우이다
이 경우에는 GC 전략 자체를 점검할 필요가 있다
그렇다면 이를 해결하려면 어떻게 해야할까?
OOM이 발생했을 때 가장 중요한 것은 원인을 정확하게 파악하는 것이다
이를 위해 다음과 같은 순서로 접근하는 것이 효과적이다
- Java 로그 확인
먼저 발생한 OOM의 종류를 확인해야 한다
발생한 로그 메시지를 통해 문제가 발생한 영역과 상황을 유추할 수 있다 - Heap Dump 분석
OOM 발생 시 Heap Dump를 생성하여 어떤 객체가 메모리를 점유하고 있는지 분석한다
-XX:+HeapDumpOnOutOfMemoryError 옵션을 통해 Dump를 생성할 수 있다
이를 분석하여 특정 객체가 비정상적으로 많은 메모리를 점유하고 있는지 확인한다 - GC 모니터링
모니터링 도구를 통한 GC 분석을 통해 메모리 회수 흐름을 확인한다
이를 통해 GC가 정상적으로 동작하고 있는지 판단할 수 있다 - 코드 레벨 개선
문제가 되는 객체 생성 패턴이나 메모리 누수를 유발하는 구조를 수정한다 - GC 튜닝
마지막으로 필요에 따라 JVM 설정을 조정한다
Heap Size 조정, GC 변경, Pause 목표 설정 등이 이에 해당한다
다만, 이 단계는 근본 원인을 해결한 이후에 보조적으로 수행하는 것이 바람직하다
'탐구일지 > Java' 카테고리의 다른 글
| AtomicLong은 어떻게 Lock-Free를 달성했을까? (0) | 2026.07.30 |
|---|---|
| G1 GC는 왜 기본 GC가 되었을까? (0) | 2026.04.24 |
| JVM Tracing GC를 이해하려다 CPU Cache까지 간 이야기 (0) | 2026.03.29 |
| JVM은 어떻게 플랫폼 독립성과 성능을 동시에 잡았을까? (2) | 2026.03.26 |
| 영속성 컨텍스트(Persistence Context)의 이해와 활용 (2) (0) | 2026.01.25 |