J-S-08 자바 성능의 핵심 JVM 힙 영역과 GC 동작 원리
글 정보
- 카테고리
- Programming/Java/Starter
- 태그
- JavaLevel2
1. JVM, GC 그리고 객체
JVM이 관리하는 메모리 영역 중, 애플리케이션 성능에 가장 지대한 영향을 미치는 곳은 단연 힙(Heap) 영역입니다.
우리가 new 키워드로 생성하는 대부분의 객체가 이곳에 저장되기 때문입니다.
이 거대한 힙 영역을 효율적으로 관리하기 위해 JVM은 가비지 컬렉터(GC) 를 고용했습니다.
GC는 더 이상 사용되지 않는 객체를 찾아내고 지우는 역할을 수행합니다.
그렇다면 GC는 어떤 기준으로 메모리를 관리할까요? 여기서 등장하는 것이 바로 '세대 단위 컬렉션 이론'입니다.
2. 세대 단위 컬렉션 이론
GC는 모든 객체를 동일하게 취급하지 않습니다.
효율적인 관리를 위해 객체의 생존 주기에 관한 몇 가지 가설을 전제로 설계되었습니다.
- 약한 세대 가설 (Weak Generational Hypothesis): 대다수의 객체는 생성되자마자 금방 쓰레기(Garbage)가 되어 사라집니다.
- 강한 세대 가설 (Strong Generational Hypothesis): GC 과정에서 살아남은 횟수가 늘어날수록, 해당 객체는 앞으로도 계속 살아남을 가능성이 높습니다.
- 세대 간 참조 가설: 서로 다른 세대(Old ↔ Young)에 속한 객체 간의 참조는 같은 세대 간의 참조보다 훨씬 적습니다.
이러한 가설은 실제 통계로도 증명되었습니다. IBM의 연구에 따르면, 일반적으로 첫 번째 GC 수행 시 약 98%의 객체가 소멸한다고 합니다. HotspotVM은 이 점을 착안하여 Eden 영역과 Survivor 영역의 비율을 8:1:1로 설계했습니다.
3. Heap 영역의 상세 구조
힙 영역은 객체의 생존 기간에 따라 크게 Young Generation과 Old Generation으로 나뉩니다.
3-1. Young Generation (젊은 세대)
생명 주기가 짧은 객체들이 머무는 곳입니다.
- Eden: 객체가 생성되자마자 처음 저장되는 곳입니다. 이곳이 꽉 차면 Minor GC가 발생합니다.
- Survivor Space (S0, S1): Eden에서 살아남은 객체들이 이동하는 곳입니다.
- Copy & Scavenge 알고리즘: Minor GC가 발생하면 Eden과 사용 중인 Survivor 영역(S0)에서 살아남은 객체들을 비어있는 Survivor 영역(S1)으로 모두 이동시킵니다.
- 파편화 방지
- 메모리 파편화란?
- 메모리 공간은 남아있지만 연속적이지 않아 큰 객체를 할당하지 못하는 상태
- Survivor 영역을 두 개로 나누어 한쪽으로 몰아넣는 방식을 사용하는 이유는 바로 이 파편화를 방지하고 메모리를 차곡차곡 정렬하기 위함
- Age bit: 객체가 Survivor 영역을 오갈 때마다 나이(Age)를 먹습니다. 특정 임계값(MaxTenuringThreshold)을 넘기면 Old Generation으로 승진(Promotion)합니다.
3-2. Old Generation (오래된 세대)
Young Generation에서 오랫동안 살아남은 객체들이 이동해 오는 영역입니다.
- 객체의 크기가 크거나 생명 주기가 긴 객체들이 보관됩니다.
- 이곳이 가득 차면 Major GC (혹은 Full GC) 가 발생합니다.
- 주로 Mark & Compact 알고리즘을 사용하여 메모리를 정리합니다.
3-3. Permanent & Metaspace (메타 정보 영역)
- *Permanent (Java 7 이전):
- 클래스와 메소드의 메타 정보를 저장하던 곳으로, 힙 영역의 일부였습니다.
- 크기가 고정되어 있어
OOM(Out Of Memory)의 주원인이 되기도 했습니다. - *Metaspace (Java 8 이후):
- Permanent 영역이 사라지고 대체된 영역입니다.
- Native Memory 사용
- JVM 힙이 아닌 OS가 관리하는 네이티브 메모리를 사용하므로, 시스템 메모리가 허용하는 한 자동으로 크기가 확장됩니다.
- 이로 인해 힙 사이즈 설정과 별개로 메모리 사용량이 늘어날 수 있음을 인지해야 합니다.
- Spring 프레임워크처럼 리플렉션(Reflection)을 많이 사용하는 경우, 런타임에 클래스 정보가 많이 로드되어 이 영역을 많이 사용하게 됩니다.
4. JVM 메모리 옵션 가이드
서버를 운영할 때 자주 보게 되는 JVM 옵션들을 정리했습니다. 각 옵션이 서로 겹치거나 충돌할 때 우선순위가 적용될 수 있으니 주의가 필요합니다.
-Xms: 힙 영역의 초기 사이즈를 설정합니다.-Xmx: 힙 영역의 최대 사이즈를 설정합니다.- 보통 운영 환경에서는 힙 크기 변경에 따른 오버헤드를 줄이기 위해
-Xms와-Xmx를 동일하게 설정하는 것을 권장합니다. -Xmn/-XX:NewSize: Young Generation의 크기를 설정합니다.-Xmn은 전체 크기를 고정하는 것이고,-XX:NewSize는 초기 크기를 지정합니다.-XX:MaxPermSize- Java 8부터는 무시되거나 경고가 발생하는 옵션입니다.
- 대신
-XX:MaxMetaspaceSize를 사용하여 메타스페이스의 상한선을 두는 것이 안전합니다.
운영 팁: 헬스 체크와 STW 서버의 상태를 확인하는 L4/L7 스위치의 헬스 체크(Health Check)가 실패하는 경우가 있습니다.
만약 힙 메모리 사용량을 기준으로 헬스 체크를 하거나, Full GC로 인해 Stop The World(STW, 모든 작업 중단) 시간이 길어지면, 서버는 멀쩡히 살아있음에도 불구하고 '응답 없음'으로 간주되어 서비스에서 제외될 수 있습니다.
GC 튜닝 시 이 점을 꼭 고려해야 합니다.
5. 대표적인 GC 알고리즘의 진화
GC는 역사가 깊은 만큼 다양한 알고리즘으로 발전해 왔습니다.
5-1. Mark and Sweep (표시하고 쓸어담기)
1960년대에 제안된 가장 기초적인 방식입니다.
- 동작: 사용 중인 객체를 식별(Mark)하고, 나머지 객체를 제거(Sweep)합니다.
- 단점: 객체를 지운 자리가 듬성듬성 비게 되어 메모리 파편화(Fragmentation) 가 발생합니다.
5-2. Mark and Copy (표시하고 복사하기)
Mark and Sweep의 파편화 문제를 해결하기 위해 1969년 로버트 페니첼이 제안했습니다.
- 동작: 메모리를 두 영역으로 나눈 뒤, 한쪽만 사용합니다. 꽉 차면 살아남은 객체만 다른 쪽으로 복사하고, 기존 영역을 싹 비웁니다.
- 단점: 전체 메모리의 절반밖에 사용하지 못한다는 비효율이 있었습니다.
- 개선: 1989년 앤드류 아펠이 이를 개선하여 Eden, S0, S1의 비율을 8:1:1로 제안했습니다. (이 방식이 현재 Young Gen의 표준 모델이 되었습니다. 전체의 90%를 사용하면서도 Copy 방식의 이점을 취할 수 있습니다.)
5-3. Mark and Compact (표시하고 압축하기)
Old Generation 처럼 객체가 많이 살아남는 영역을 위해 1974년 에드워드 루더스가 제안했습니다.
- 동작: Mark 후, 살아남은 객체들을 한쪽 구석으로 몰아넣어(Compact) 빈 공간을 연속적으로 만듭니다.
- 단점: 객체를 이동시키는 과정은 비용이 많이 듭니다. 특히 이 과정에서 애플리케이션이 멈추는 Stop The World가 발생하며, 살아남은 객체가 많을수록 중단 시간은 길어집니다.