자바는 메모리 관리를 어떻게 할까 그리고 왜 그렇게 할까?

이상현·2025년 8월 6일
0

Java

목록 보기
23/23
post-thumbnail

다양한 메모리 관리 기법

모든 프로그래밍 언어는 프로그래밍이 실행될 때 변수/함수/객체 등을 할당과 해제를 반복하며 메모리를 관리해야 한다. 그렇지 않으면 메모리가 가득 차서 프로그램이 멈출것이다.

이를 위해서 프로그래밍 언어들은 각각의 목적에 맞게 메모리를 다양한 방법으로 관리한다.

대표적으로 다음과 같이 두가지로 나눌 수 있다.

방식설명예시 언어장점단점
수동 관리개발자가 명시적으로 메모리를 할당하고 해제해야 함C, C++세밀한 제어로 성능 좋음개발자 실수 가능성 높음
자동 관리언어 또는 런타임이 메모리 회수를 자동으로 처리함Java, Python, Go, JavaScript, Swift메모리 안정성 높음런타임 오버헤드 발생

좋고 나쁜건 없다. 모든 언어는 목적이 있고, 모든 방식에는 트레이드 오프가 있다. 수동 관리 언어는 자원이 제한적인 임베디드에서 강점을 보이고, 자동 관리 언어는 안정성이 높아야하는 조건에서 강점을 보인다.

수동 관리 기법이 무조건 구식이라고 할 수도 없다. 대표적인 메모리 자동 관리 방법인 Garbage Collection은 1959년에 LISP라는 언어에서 처음 등장했고, C언어는 그로부터 10년도 더 지나서야 나왔다.

각 언어별 메모리 관리 기법

대표적인 언어들이 어떤 메모리 관리 기법을 사용하는지 간단하게 알아보자.

언어메모리 관리 방식특징
C수동 (malloc/free)빠르지만 위험함 (메모리 누수, 댕글링 포인터)
C++수동 + RAII (스마트 포인터)unique_ptr, shared_ptr 등으로 자동화 보조
Java자동 (GC)세대별 GC, 병렬/정지 시간 최적화
Python자동(참조 카운팅 + 순환 참조 감지 GC)일관되지만 순환 참조 해소를 위해 GC 도입
Rust반자동 (소유권 모델)GC 없이도 메모리 안전성 보장, 문법 숙지 필요
Swift자동 (참조 카운팅 + ARC)객체 수명 예측 쉬움, 순환 참조는 weak 등으로 방지
Kotlin자동 (GC, JVM 기반)Java와 유사한 GC 사용, Kotlin/Native는 참조 카운팅

자바는 Garbage Collection 을 사용한다고 한다. 그것이 뭔지 알아보자.

Garbage Collection

가비지 컬렉션 (GC) 는 프로그램이 더 이상 사용하지 않을것 같은 메모리를 정리하는 시스템이다.

가비지 컬렉터라는 기법이 나온 이유를 Bottom-up 방식으로 알아보자.

먼저, LISP의 특징을 살펴보자

LISP는 1958년에 등장한 최초의 함수형 언어 중 하나이다.

  • 실행 중에 리스트나 함수 같은 객체를 많이 만들어내는 언어다.
  • 대부분의 데이터가 힙(heap)에 할당됨

-> LISP 프로그램은 실행 도중에 끊임없이 새로운 리스트와 함수 객체들을 만들어낸다.

그래서 어떤 문제가 있었을까?

  • 수동으로 해제한다면, 실수로 아직 필요한 객체를 해제하면 댕글링 포인터 오류, 해제를 못하면 메모리 누수 발생

-> 일일이 수동으로 관리하기엔 비효율적이고, 오류 유발 가능성이 너무 컸다.

GC 등장

위 문제를 해결하기 위해, 인공지능 이라는 용어를 처음 차용한 사람이기도 한 John McCarthy 는 LISP 의 메모리 문제를 해결하기 위해

"더 이상 사용할 수 없게 된 메모리 영역을 탐지하여 자동으로 해제하는 기법" 인 GC 알고리즘을 최초로 개발했다.


가비지 컬렉터 를 시각적으로 보여주는 GIF (출처: wikipedia)

어떤 방식으로 구현한걸까?

한개 이상의 변수가 접근 가능한 메모리는, 사용할 수 있는 메모리로 간주하고, 나머지 메모리를 해제한다.

Mark and Sweep

가장 단순한 기법이다.

  1. 각 메모리 할당 영역에 표시를 위한 1비트의 메모리를 남긴다.
  2. 전체 시스템의 실행을 정지한다. (Stop The World)
  3. Mark 단계: 모든 변수가 가리키는 영역을 "사용중" 으로 표시한다. 그 영역이 카리키는 또 다른 영역 또한 마찬가지이다.
  4. Sweep 단계: 표시되지 않는 영역을 모두 해제한다.

단점

Mark 단계에서 메모리 내용이 변경되지 않아야 하기 때문에 Stop The World 현상이 발생한다.
또한, Sweep 단계에서, 메모리 영역 전체를 검사해야 하므로 메모리 페이징을 사용하는 운영체제에서 성능이 저하될 수 있다.

개선 버전 - 세대 단위로 영역 나누기

GC를 지속적으로 사용한 결과, 연구자들은 프로그램에서 새롭게 할당된 영역일 수록 금방 해제될 확률이 높다는 것이 관찰했다. 이런 특성을 활용하여 각각의 객체를 할당된 시간에 따라 세대별로 구분하여 각 세대별로 서로 다른 메모리 영역에 객체를 할당한다. 새로 할당된 영역에서는 대부분의 객체들이 빠르게 해제되므로 메모리의 일부 영역만을 주기적으로 수집하게 되어 효율적으로 개선되었다.

JVM 의 Heap 구조

JVM 의 힙은 동적으로 메모리가 할당되는 메모리 공간으로, GC의 대상이 되는 공간이다.
위 이론을 토대로, JVM 의 Heap 은 다음과 같은 영역으로 나누어졌다.

영역설명GC 종류
Young 영역새로운 객체들이 먼저 할당되는 공간. 대부분의 객체가 이곳에서 짧은 생애를 가짐Minor GC
Old 영역Young 영역에서 일정 시간 이상 살아남은 객체들이 이동해오는 공간. 오래 살아남는 객체가 많음Major GC

또한, Young 영역은 3가지 영역으로 더 나누어져 구성된다.

영역설명특징
Eden새로 생성된 객체가 할당되는 공간한번 살아남으면 Survival 영역으로 보냄
Survival 0, 1GC에서 살아남은 객체들이 이동하는 공간두 영역중 하나는 비어있어야 함

Minor GC 과정

Minor GC는 Young 영역에서만 발생하는 가비지 컬렉션이다.
짧은 생애를 가진 객체들을 빠르게 정리하기 위한 최적화된 GC 방식이다.

  1. Eden 영역이 가득 차면 Minor GC 발생
  2. GC 실행 과정:
    • Eden 영역에서 더 이상 참조되지 않는 객체를 제거
    • 살아남은 객체는 Survivor 0 또는 1 영역으로 이동 (age 값 증가)
    • 기존 Survivor 영역에 있던 객체 중, 한 번 더 살아남은 객체는 나머지 Survivor로 복사 (age 값 증가)
  3. 객체가 여러 번 살아남으면 (age 임계값 도달) Old 영역으로 이동 (Promotion)

Survivor 0 <-> Survivor 1 은 매 GC마다 역할이 바뀜 (ping-pong 방식)

Major GC 과정

Major GC 또는 Full GC는 Old 영역에서 수행되는 GC이다.
Minor GC보다 비용이 크고 느리다. 오래 살아남은 객체의 정리를 담당한다.

  1. Old 영역이 일정 수준 이상 가득 차면 Major GC 발생
  2. GC가 Old 영역을 검사하여, 더 이상 참조되지 않는 객체 제거
  3. 필요에 따라:
    • 힙을 압축(compaction)하여 조각화된 메모리를 정리 (Mark-Compact 방식 사용 가능)
    • 전체 힙 검사도 포함되는 경우 → Full GC

Major GC는 애플리케이션 Stop-The-World(정지) 시간이 길 수 있으므로 GC 튜닝과 JVM 파라미터 설정이 중요하다.
Minor GC 보다 몇배 이상 오래 걸린다.

Java에서의 Garbage Collection

James Gosling 이 주도한 Java 의 개발 목표는, 가상 머신 위에서 동작하는 언어였다.

그걸 가능하게 한 게 JVM 이라는 가상 머신이다. 즉, 자바는 OS나 하드웨어에 신경 쓰지 않고, 추상화된 실행 환경만 고려하면 됐다. 그런데, 가상 머신 위에서는 메모리 구조도 가상화 되어있기 때문에, OS 의 메모리 관리 기능과 직접적으로 상호작용 하지 않는다. 즉, OS에 의존하지 않고 안정적으로 객체 수명을 관리하는 기능이 필요했고, GC를 애초에 염두에 두고 개발하기 시작했다.

그렇다면, 자바의 가비지 컬렉션은 어떤식으로 구현되어있는지 살펴보자.

Serial Collector

  • GC 동작에 싱글 스레드를 사용한다. 쓰레드 간의 정보 전달 오버헤드가 없어 효율적이다.
  • 단일 프로세서 머신에 적합하며, 약 100MB 이하의 작은 데이터셋에서는 멀티 프로세서에서도 유용할 수 있다.
XX:+UseSerialGC

Parallel Collector

  • Throughput Collector라고도 불리며, Serial Collector와 유사한 세대별 수집기이다.
  • 여러 스레드를 사용하여 GC를 병렬로 수행하므로, 전체 처리량(Throughput)이 중요할 때 적합하다.
  • 멀티프로세서 환경에서 중간~대용량 데이터셋에 적합하다.
-XX:+UseParallelGC

The Mostly Concurrent Collectors (CMS)

  • 대부분의 GC 작업을 애플리케이션 스레드와 동시에(Concurrent) 수행하여 pause time을 줄이려는 수집기.
  • GC가 짧게 자주 일어나고, 응답 시간이 중요한 애플리케이션에 적합하다.
  • JDK 9부터는 deprecated 되었음.
-XX:+UseConcMarkSweepGC

Garbage-First (G1) garbage collector

  • Server 환경을 위한 GC로, 멀티프로세서 시스템과 큰 힙 메모리에 적합하다.
  • Pause time 목표를 달성하면서도 높은 throughput을 유지하는 데에 초점이 있다.
  • 대부분의 GC 작업을 애플리케이션과 동시에 수행하며, GC pause 시간 예측이 용이하다.
-XX:+UseG1GC

The Z Garbage Collector (ZGC)

  • 확장 가능하고 지연 시간이 낮은 GC이다.
  • 모든 비용이 큰 작업을 애플리케이션 스레드를 멈추지 않고 동시 수행한다.
  • 10ms 이하의 짧은 GC 중단 시간 또는 수 테라바이트에 이르는 매우 큰 힙을 요구하는 애플리케이션에 적합하다.
  • JDK 11부터 experimental 기능으로 제공되며, -XX:+UseZGC 옵션으로 활성화 가능하다.
-XX:+UseZGC

콜렉터 선택법

  • 특별히 GC 지연 시간(pause time)에 민감하지 않다면, 기본 설정으로 애플리케이션을 먼저 실행해보고 JVM이 선택한 GC를 사용하는 것이 좋다.

성능이 목표에 못 미친다면 힙 크기를 조절하고, 필요 시 아래 지침을 따라 GC를 수동으로 선택할 수 있다

조건추천 GCJVM 옵션
데이터셋이 작음 (약 100MB 이하)Serial Collector-XX:+UseSerialGC
단일 프로세서에서 실행되며 pause time에 민감하지 않음Serial Collector-XX:+UseSerialGC
최대 처리량이 가장 중요하고, 1초 이상 pause 허용 가능Parallel Collector-XX:+UseParallelGC
응답 시간이 throughput보다 중요하고, pause를 1초 미만으로 유지해야 함G1 또는 CMS-XX:+UseG1GC or -XX:+UseConcMarkSweepGC
응답 시간이 매우 중요하거나, 테라바이트 수준의 힙 사용ZGC-XX:+UseZGC

원하는 성능이 나오지 않는 경우, 먼저 힙/세대 크기를 조절해보고, 그래도 부족하다면 pause-time 줄이려면 CMS/G1, 처리량을 늘리려면 Parallel Collector를 시도한다.

출처
ChatGPT
Wikipedia - 쓰레기 수집
Java Oracle Docs - Available Collectors

1개의 댓글

comment-user-thumbnail
2025년 9월 22일

잘 보고 갑니다~

답글 달기