과거의 언어들은 동적 메모리 할당 기능이 없었거나,
FORTRAN
이나C
와 같은 언어에서는개발자가 메모리를 할당하고 해제하는 작업을 직접 수행
했었습니다.그러나 이러한
수동 관리
는 인간의 실수로 인해메모리 누수가 발생
하거나,메모리를 잘못 사용
하거나,중복으로 해제
하는 등의 버그를 유발했습니다.
이러한 버그들은 프로그램의 예상치 못한 동작
이나 비정상 종료를 초래
할 뿐만 아니라, 보안 취약점
으로 이어질 수도 있습니다.
실제로, Microsoft에 따르면 윈도우 보안 취약점의 약 70%가 메모리와 관련된 문제로 발생한다고 합니다.
이러한 문제를 해결하기 위해서 제시된 것이 가비지 컬렉션입니다.
가비지 컬렉션은 프로그래머가 직접적으로 메모리를 할당하고 해제하는 것 대신,
프로그램 실행 중에 더 이상 필요하지 않은 메모리를 자동으로 식별하고 해제
합니다.Heap 영역에서 동적으로 할당했던 메모리 중 필요 없게 된 메모리 객체(garbage)를 모아 주기적으로 제거
이를 통해
프로그래머는 메모리 관리에 대한 부담을 줄이고
,
런타임 시스템이 자동으로 메모리를 관리
를 하도록 할 수 있습니다.
따라서 가비지 컬렉션은 프로그램이 실행되는 동안 발생하는 쓸모 없어진 메모리를 효율적으로 관리하여 프로그램의 성능과 안정성을 향상
시키는 데 도움을 줍니다.
Java에서는 가비지 컬렉터를 이용해 Java 프로세스는
한정된 메모리를 효율적으로 사용
할 수 있고, 개발자 입장에서는메모리 관리
,메모리 누수(Memory Leak)
문제에서 대해관리하지 않아도 되서 개발에만 집중
할 수 있습니다.
또한 Java 뿐만 아니라 여러 프로그래밍 언어에서 사용되는 일반적인 개념입니다.
예를 들어, Python, JavaScript, Go와 같은 다양한 언어에서도 가비지 컬렉션 기능이 내장되어 있습니다.
for (int i = 0; i < 9999; i++) { NewObject obj = new NewObject(); obj.something(); }
위 코드를 살펴보면,
반복문 내에서 생성된 객체들
은 루프가 종료된 이후에는 더 이상 사용되지 않지만 메모리를 계속 점유
하게 됩니다.
이러한 상황에서, 메모리 자원은 점차적으로 고갈
될 수 있습니다.
그러나, Java
에서는 가비지 컬렉션(GC)이라는 메커니즘
이 존재하여 이러한 일회성 객체들을 주기적으로 정리
함으로써, 한정된 메모리 자원을 효율적으로 관리
할 수 있습니다."
가비지 컬렉션에도 단점이 존재합니다.
가비지 컬렉션은 메모리를 자동으로 관리해주지만,
메모리가 언제 정확히 해제될지 예측하기 어렵습니다.
또한, 가비지 컬렉션동작 중에는 프로그램이 일시적으로 멈출 수 있어 성능에 영향
을 줄 수 있습니다.
특히 대규모 프로그램이나 실시간 요구
에 민감한 응용 프로그램에서는 이러한 중단이 문제
가 될 수 있으므로, 가비지 컬렉션을 사용할 때는 이러한 제약사항을 고려
하여 프로그램을 설계
해야 합니다.
이러한 현상을 Stop-The-World
라고 합니다.
GC
가 메모리를 회수하기 위해 작업을 수행
할 때,
정확한 메모리 회수를 위해 Java 가상 기계(JVM)가 관리하는 모든 애플리케이션 스레드를 일시 정지시키는 현상을 Stop-The-World (STW)
라고 합니다.
이 현상은 GC가 실행되는 동안 프로그램의 실행이 멈추게 되므로, 애플리케이션의 응답성이 저하
됩니다. 따라서, STW의 시간을 가능한 한 최소화
하는 것이 중요합니다.
가비지 컬렉션이 너무 자주 실행될 경우, 소프트웨어의 성능이 저하될 수 있습니다. 예를 들어, 과거 인터넷 익스플로러
는 가비지 컬렉션을 너무 자주 실행하여 성능 문제
를 일으키는 것으로 악명이 높았습니다.
실시간 성능이 중요한 프로그램의 경우, 가비지 컬렉션에 의존하는 것이 적합하지 않을 수 있습니다. 이에 따라, 애플리케이션의 사용성을 유지하면서 메모리 관리를 효율적
으로 수행할 수 있도록 가비지 컬렉션을 최적화하는 작업, 즉 GC 튜닝이 개발자에게 중요한 과제
가 됩니다.
GC 튜닝은 가비지 컬렉션의 효율을 높이고 STW의 지속 시간을 최소화하여, 애플리케이션의 응답성과 성능을 개선하기 위해 사용되며,
해당 포스팅에서는 GC 튜닝을 다루지는 않습니다 : )
GC는 특정 객체가 garbage인지 아닌지 판단
하기 위한도달능력(Reachability)
Reachable(도달 가능)
참조된 객체들을 통해 애플리케이션 코드에서 접근할 수 있는 상태
사용 중이거나 사용될 가능성이 있으므로 GC에 의해 회수되지 않음
Unreachable(도달 불가능)
애플리케이션 코드에서 더이상 그 객체에 접근할 방법이 없음
을 의미더 이상 사용되지 않으므로 GC의 대상이 되어 메모리에서 회수될 수 있음
도달 가능성의 판단 기준은 GC가 메모리 관리를 효율적으로 수행하는 데 핵심적인 역할을 합니다.
객체의 도달 가능 상태
와 도달 불가능 상태
를 구분
함으로써,
GC는 메모리 내의 해당 객체를 안전하게 회수
할 수 있습니다.
이 과정에서,
객체에 유효한 레퍼런스가 존재하면 해당 객체는 Reachable
로 간주되어 회수되지 않습니다.
반면, 유효한 레퍼런스가 없는 객체는 Unreachable
로 구분되어 GC의 대상
이 됩니다.
이를 통해 JVM은 메모리를 효율적으로 관리하고 메모리 누수를 방지하여 애플리케이션의 성능을 유지할 수 있습니다.
도달 가능성에 따라
GC
가Unreachable한 객체를
어떻게(?) 청소
하는지 알아보겠습니다.
Mark & Sweep은 다양한 가비지 컬렉션(GC)에서 사용되는 기본적인 객체 회수 알고리즘입니다.
Mark and Sweep 방식
에서는 객체가 루트로부터 접근 가능한지 여부를 기준으로 메모리 해제를 결정합니다.
JVM
의Root Space
는
메소드 영역
,정적 변수
,스택
,네이티브 메소드 스택
이
참조하는 힙 메모리를 포함
합니다.
이 방법은 세 단계로 구성됩니다.
첫 번째 단계인 Mark
단계에서는, GC
가 도달 가능한 모든 객체를 탐색
하며 표시합니다.
이 과정에서 애플리케이션의 루트 집합에서 출발하여 참조를 따라가면서 도달할 수 있는 모든 객체에 표시를 남깁니다.
두 번째로, Sweep
단계에서는 표시되지 않은, 즉 도달 불가능한 객체들을 메모리에서 제거
합니다.
마지막 Compact
과정은 Sweep
단계 후에 발생하며,
메모리 내의 분산된 객체들을 힙의 시작 주소로 모아
서 메모리의 사용된 부분과 사용되지 않은 부분을 명확하게 분리
합니다.
이 압축 과정을 통해, 메모리 할당 및 접근 속도를 향상
시키고, 메모리의 효율적인 사용
을 가능하게 합니다.
(GC 종류에 따라 하지 않는 경우도 있음)
Mark 과정에서는
Root Space
에서 시작하여그래프 순회
를 통해연결된 모든 객체를 탐색
하고, 찾아낸 객체들을마킹
합니다.이 객체들은 그림상에서
초록색 원으로 표시
됩니다.
마킹 전
마킹 후
Sweep 과정
에서는Heap 내의 Unreachable 객체들
, 즉 참조되지 않는객체들을 제거
합니다.이 객체들은 그림상에서
파란색 원으로 표시
됩니다.
삭제된 모습
Compact 과정
에서는Sweep 후
,분산된 객체들을 Heap의 시작 주소로 모아 메모리를 압축
합니다.
JVM
의Heap
영역은 동적으로 생성된 객체들이 저장되는 곳으로, GC의 주요 대상이 되는 메모리 공간입니다.
이 영역의 설계는 약한 세대 가설(Weak Generational Hypothesis)
을 기반으로 하며, 다음 두 가지 주요 가정에 기초하게 됩니다.
객체들이 빠르게 생성되고 소멸
되며, 대부분 일회성
으로 사용됩니다.시간이 지남에 따라 객체 간의 참조 관계는 대부분 크게 변하지 않으며
,새 객체들은 오래된 객체들에 의해 참조되는 경우가 적습니다.
이러한 두 가지 주요 가정을 바탕으로,
JVM
개발자들은 메모리 관리를 보다 효율적으로 하기 위해Heap 영역을 물리적으로 두 부분을 나누었습니다.
Young 영역(Minor GC)
: 새로 생성된 객체들이 배치
되는 곳으로,
이 객체들 중 대부분은 빠르게 Unreachable
상태가 되어 GC
에 의해 회수됩니다.
Old 영역(Major GC || Full GC)
: Old 영역은 시간이 지나며 여전히 사용되는
즉, 오래된 객체들이 저장
되는 곳으로,여기서의 객체들은 비교적 덜 자주 회수됩니다.
초기 JVM 설계
에서는Perm(영구) 영역도 존재
했으나,
이는 주로JVM의 메타데이터와 클래스 구조 같은 정적 데이터
를저장
하는 곳이었습니다.
Java 8
부터는 이Perm 영역이 제거
되고, 그 기능은Metaspace로 대체
되었습니다.
Heap 메모리 영역을 세대별로 나누는 설계
는 객체의 생존 기간을 고려한 메모리 관리 전략의 방법으로,GC의 효율성을 크게 향상
시킵니다.
효율적인 GC를 위해
Young(Minor GC)
를
3가지 영역(Eden
,Survivor 0
,Survivor 1
)으로 나눕니다.
Eden 영역
은 새로 생성된 객체들이 배치되는 곳입니다.
여기서 객체들은 처음으로 가비지 컬렉션(GC)을 겪게 되며,GC 후에 살아남은 객체들은 Survivor 영역으로 이동
합니다.
Survivor 영역
은 두 부분, Survivor 0
과 Survivor 1
으로 나뉘며, 최소 한 번의 GC를 거쳐 살아남은 객체들이 저장
되는 곳입니다.
이 두 영역 중 하나는 항상 비어 있어야 하며,
GC
후 살아남은 객체들은 비어 있는 Survivor 영역으로 이동
합니다.
힙 영역을 세분화함으로써,
객체의 생존 기간을 정밀하게 관리하여GC
가불필요한 객체를 효과적으로 제거
합니다.
Permanent 영역
은 클래스와 메소드의 메타데이터를 저장하는 곳으로,Java 7까지 힙에 포함
되었습니다.
Java 8
부터는 이 영역이Metaspace로 대체
되어, 네이티브 메소드 스택으로 이동했습니다.
Eden
영역에 위치Eden
영역이 Full
하면 Minor GC가 실행
Mark
를 통해 Reachable 객체를 탐색
후 살아남은 객체
는 Survivor
영역으로 이동파란색으로 표시
됩니다.Eden
영역에서 사용되지 않는 객체(Unreachable)
의 메모리를 해제(Sweep)
age 값이 1 증가
Survivor 영역
에서, 객체의age 값은
해당 객체가GC를 통해 살아남은 횟수
를 나타냅니다.이 값은
객체의 헤더에 기록
되며,일정 횟수(기본적으로 31회)를 넘으면 객체는 Old 영역으로 이동
합니다.이는
HotSpot JVM
에서객체 헤더의 6비트를 사용하여 age 값을 기록하기 때문에 가능
합니다.또한,
Survivor
영역의 운영 규칙에 따라, 두 Survivor 영역 중 하나는 반드시 비어 있어야 합니다.
Old Generation
은장기간 사용되는 객체들이 모이는 메모리 영역
입니다.이곳의 객체들은 원래
Young Generation에서 시작
되었지만,여러 번의 GC를 거치며 age 임계값에 도달하여 Old 영역으로 이동
한 객체들입니다.
Old 영역에서 메모리가 부족해지면,
Major GC
가 발생하여 이 영역의 객체들을 청소합니다.
Major GC
는Old Generation 영역의 메모리가 꽉 차면 실행
되며, 이 과정에서 참조되지 않는 모든 객체들을 검사하여 일괄적으로 제거합니다.
Old Generation
은큰 메모리 공간을 차지
하기 때문에GC가 상대적으로 오래 걸립니다.
예를 들어,
Young Generation
의Minor GC
는짧은 시간 내에 완료
되지만,
Old Generation
의Major GC
는 훨씬더 긴 시간이 소요
되며시스템에 부담을 줄 수 있습니다.
이 때 발생하는
Stop-The-World 현상
은 애플리케이션의 작업을 중단시키고 성능 저하를 일으킬 수 있어, 자바 개발자들은지속적으로 GC 알고리즘을 개선
해 왔습니다.GC 알고리즘은 다음 포스팅에 이어서 진행하겠습니다.
Minor, Major Gc
차이를 표로 보기
GC 종류 | Minor GC | Major GC |
---|---|---|
대상 | Young Gerneration | Old Gerneration |
실행 시점 | Eden 영역이 꽉 찬 경우 | Old 영역이 꽉 찬 경우 |
실행 속도 | 빠름 | 느림 |
잘 읽고 갑니다~