모든 프로그래밍 언어는 프로그래밍이 실행될 때 변수/함수/객체 등을 할당과 해제를 반복하며 메모리를 관리해야 한다. 그렇지 않으면 메모리가 가득 차서 프로그램이 멈출것이다.
이를 위해서 프로그래밍 언어들은 각각의 목적에 맞게 메모리를 다양한 방법으로 관리한다.
대표적으로 다음과 같이 두가지로 나눌 수 있다.
방식 | 설명 | 예시 언어 | 장점 | 단점 |
---|---|---|---|---|
수동 관리 | 개발자가 명시적으로 메모리를 할당하고 해제해야 함 | 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 을 사용한다고 한다. 그것이 뭔지 알아보자.
가비지 컬렉션 (GC) 는 프로그램이 더 이상 사용하지 않을것 같은 메모리를 정리하는 시스템이다.
가비지 컬렉터라는 기법이 나온 이유를 Bottom-up 방식으로 알아보자.
LISP는 1958년에 등장한 최초의 함수형 언어 중 하나이다.
-> LISP 프로그램은 실행 도중에 끊임없이 새로운 리스트와 함수 객체들을 만들어낸다.
-> 일일이 수동으로 관리하기엔 비효율적이고, 오류 유발 가능성이 너무 컸다.
위 문제를 해결하기 위해, 인공지능 이라는 용어를 처음 차용한 사람이기도 한 John McCarthy 는 LISP 의 메모리 문제를 해결하기 위해
"더 이상 사용할 수 없게 된 메모리 영역을 탐지하여 자동으로 해제하는 기법" 인 GC 알고리즘을 최초로 개발했다.
가비지 컬렉터 를 시각적으로 보여주는 GIF (출처: wikipedia)
한개 이상의 변수가 접근 가능한 메모리는, 사용할 수 있는 메모리로 간주하고, 나머지 메모리를 해제한다.
가장 단순한 기법이다.
Mark 단계에서 메모리 내용이 변경되지 않아야 하기 때문에 Stop The World 현상이 발생한다.
또한, Sweep 단계에서, 메모리 영역 전체를 검사해야 하므로 메모리 페이징을 사용하는 운영체제에서 성능이 저하될 수 있다.
GC를 지속적으로 사용한 결과, 연구자들은 프로그램에서 새롭게 할당된 영역일 수록 금방 해제될 확률이 높다는 것이 관찰했다. 이런 특성을 활용하여 각각의 객체를 할당된 시간에 따라 세대별로 구분하여 각 세대별로 서로 다른 메모리 영역에 객체를 할당한다. 새로 할당된 영역에서는 대부분의 객체들이 빠르게 해제되므로 메모리의 일부 영역만을 주기적으로 수집하게 되어 효율적으로 개선되었다.
JVM 의 힙은 동적으로 메모리가 할당되는 메모리 공간으로, GC의 대상이 되는 공간이다.
위 이론을 토대로, JVM 의 Heap 은 다음과 같은 영역으로 나누어졌다.
영역 | 설명 | GC 종류 |
---|---|---|
Young 영역 | 새로운 객체들이 먼저 할당되는 공간. 대부분의 객체가 이곳에서 짧은 생애를 가짐 | Minor GC |
Old 영역 | Young 영역에서 일정 시간 이상 살아남은 객체들이 이동해오는 공간. 오래 살아남는 객체가 많음 | Major GC |
또한, Young 영역은 3가지 영역으로 더 나누어져 구성된다.
영역 | 설명 | 특징 |
---|---|---|
Eden | 새로 생성된 객체가 할당되는 공간 | 한번 살아남으면 Survival 영역으로 보냄 |
Survival 0, 1 | GC에서 살아남은 객체들이 이동하는 공간 | 두 영역중 하나는 비어있어야 함 |
Minor GC는 Young 영역에서만 발생하는 가비지 컬렉션이다.
짧은 생애를 가진 객체들을 빠르게 정리하기 위한 최적화된 GC 방식이다.
Survivor 0 <-> Survivor 1 은 매 GC마다 역할이 바뀜 (ping-pong 방식)
Major GC 또는 Full GC는 Old 영역에서 수행되는 GC이다.
Minor GC보다 비용이 크고 느리다. 오래 살아남은 객체의 정리를 담당한다.
Major GC는 애플리케이션 Stop-The-World(정지) 시간이 길 수 있으므로 GC 튜닝과 JVM 파라미터 설정이 중요하다.
Minor GC 보다 몇배 이상 오래 걸린다.
James Gosling 이 주도한 Java 의 개발 목표는, 가상 머신 위에서 동작하는 언어였다.
그걸 가능하게 한 게 JVM 이라는 가상 머신이다. 즉, 자바는 OS나 하드웨어에 신경 쓰지 않고, 추상화된 실행 환경만 고려하면 됐다. 그런데, 가상 머신 위에서는 메모리 구조도 가상화 되어있기 때문에, OS 의 메모리 관리 기능과 직접적으로 상호작용 하지 않는다. 즉, OS에 의존하지 않고 안정적으로 객체 수명을 관리하는 기능이 필요했고, GC를 애초에 염두에 두고 개발하기 시작했다.
그렇다면, 자바의 가비지 컬렉션은 어떤식으로 구현되어있는지 살펴보자.
XX:+UseSerialGC
-XX:+UseParallelGC
-XX:+UseConcMarkSweepGC
-XX:+UseG1GC
-XX:+UseZGC
성능이 목표에 못 미친다면 힙 크기를 조절하고, 필요 시 아래 지침을 따라 GC를 수동으로 선택할 수 있다
조건 | 추천 GC | JVM 옵션 |
---|---|---|
데이터셋이 작음 (약 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
잘 보고 갑니다~