2023. 11. 6. 13:45ㆍJava
이번 포스팅을 통해 자바 변수 타입별 메모리 저장 위치 및 GC의 동작원리에 대해 살펴보겠습니다.
변수의 종류
변수란 하나의 값을 저장할 수 있는 메모리 공간입니다. 변수의 종류에는 primitive type과 reference type이 있으며 기본형 8개를 제외하면 모두 reference type입니다.
primitive type (기본형 타입)
boolean, char, byte, short, int, long, float, double
변수 저장 위치
primitive type과 reference type은 메모리 어느 영역에 저장이 될까요?
primitive type 변수는 stack 영역, reference type변수는 heap 영역에 저장되는데, 그렇다면 여기서 추가적으로 드는 의문이 있습니다.
자바에서 GC는 필요 없는 객체를 알아서 찾아서 삭제한다는 특징이 있는데,
🤔 그렇다면 primitive type 변수와 reference type변수 모두 GC가 알아서 지워주는 것일까요?
저는 처음에는 YES라고 생각했습니다. 단순하게 GC는 우리가 모르는 영역에서 더 이상 참조하지 않는 객체를 마법사처럼 찾아내서 알아서 삭제해 준다고 생각했으니까요. 하지만 답은 NO입니다.
간략히 설명하자면, GC는 Heap영역만 관장하기 때문인데요. 그렇다는 건 reference type변수만 GC가 관리한다는 의미가 됩니다.
그럼 이제부터는 본격적으로 GC에 대해 알아봐야 할 것 같습니다. 크게 다음의 순서로 설명할 예정입니다.
- GC의 중요성 및 정의
- 가비지 객체 판별법
- 가비지 객체 처리 방식
- GC는 만능일까?
- Heap메모리 구조 파헤치기
- GC알고리즘 종류 및 장·단점
GC를 공부해야 하는 이유
JVM은 메모리를 자동으로 관리해 줍니다. 이러한 이유로 GC는 성능 입장에서 매우 중요합니다.
만약 더이상 사용하지 않는 객체들이 메모리에 계속 쌓이게 된다면 OOME (Out of Memory Error)에 빠질 가능성이 있습니다.
또한, GC를 수행할 때 다른 Thread의 작업이 모두 멈추기 때문에 수 초 동안 애플리케이션 정지되면, 이는 여러 장애로 번질 가능성이 있으므로 프로그램 설계단계부터 GC도 함께 고려하면서 작업해야 합니다.
GC (Garbage Collector)
GC는 자바의 메모리 관리 기법 중 하나로 JVM(자바 가상 머신)의 Heap 영역에서 동적으로 할당했던 메모리 중 더이상 사용하지 않는 객체를 주기적으로 제거하는 과정입니다.
1. Heap 내의 객체 중에서 GC 대상 객체를 찾고 (Garbage 대상 식별)
2. 대상 객체를 처리(finalization)하고, 할당된 Heap 메모리를 회수하는 작업으로 구성된다.
말은 쉽지만,,, 위의 문장에는 가비지 객체 판별 방법, 알고리즘 및 동작과정이 담겨있습니다.
하나씩 살펴보겠습니다.
가비지 객체 판별법, Reachability
GC는 객체가 가비지인지 판별하기 위해 reachability를 사용합니다.
객체에 유효한 참조가 있으면 'reachable', 없으면 'unreachable'로 구별한 후 unreachable 객체를 가비지로 객체로 간주해 GC를 수행하게 됩니다.
여기서 객체는 단 방향 으로 참조하는 게 아닌 여러 객체를 서로 참조 가능하기에 사슬처럼 이어져 있습니다.
여러 객체가 이어져 있더라도 처음은 존재하는데, 이를 root set이라 합니다.
이해를 돕기위해 잘 표현된 그림 한 장을 가져왔습니다.
JVM에서 메모리 영역인 런타임 데이터 영역(runtime data area)의 구조
런타임 데이터 영역은 세 부분으로 나뉠 수 있습니다.
1. 스레드에서 시작하는 부분
2. 객체를 생성 및 보관하는 힙 영역
3. 클래스 정보가 차지하는 영역인 메서드 영역
root set으로부터 시작한 참조 사슬에 속한 객체들은 reachable 객체이고, 이 참조 사슬과 무관한 객체들이 unreachable 객체로 GC 대상입니다.
GC가 Unreachable한 객체를 처리(finalization)하는 방식은?
Mark and Sweep 동작 알고리즘
- Mark 과정 : 먼저 Root Set로부터 그래프 순회를 통해 모든 변수를 스캔하며 각각 어떤 객체를 참조하고 있는지 찾아서 마킹한다.
- Sweep 과정 : Unreachable 객체들을 Heap에서 제거한다.
- Compact 과정 : Sweep 후에 분산된 객체들을 Heap의 시작 주소로 모아 메모리가 할당된 부분과 그렇지 않은 부분으로 압축한다. (선택)
이렇게 Mark And Sweep 방식을 사용하면 루트로부터 연결이 끊긴 순환 참조되는 객체들을 모두 지울 수 있습니다.
그럼 GC는 만능일까?
우리는 객체를 생성할 뿐 처리하진 않습니다. 이것이 가능한 이유는 GC가 가비지 객체를 선별해서 처리하여 메모리를 효율적으로 사용할 수 있도록 하기 때문입니다.
그렇다면 GC는 만능일까요? 장점이 존재하면 단점도 존재합니다.
GC가 동작하는 동안에는 관련 Thread를 제외한 모든 Thread는 멈추는 Stop-The-World가 발생합니다.
일시적으로 중단된 스레드들은 GC가 완료될 때까지 대기해야 하므로 "Stop the World" 이벤트는 애플리케이션의 응답 시간에 영향을 미칠 수 있으며, 이로 인해 성능 문제가 발생할 수 있습니다.
GC의 목표는 메모리 누수를 방지하고 응용 프로그램의 성능을 최적화하는 것이지만, "Stop the World"가 빈번하거나 긴 지속 시간을 가지면 장애로 이어질 수 있으므로 GC 튜닝과 관련된 성능 최적화 작업이 필요할 수 있습니다.
GC튜닝이란,
Stop-The-World 시간을 줄이는 것.
GC의 대상이 되는 공간, Heap 메모리 구조 살펴보기
Heap영역은 두 가지를 전제 조건 (Weak Generational Hypothesis)을 가지고 있습니다.
- 대부분의 객체는 금방 접근 불가능한 상태(Unreachable)가 된다.
- 오래된 객체에서 새로운 객체로의 참조는 아주 적게 존재한다.
이 가설의 장점을 최대한 살리기 위해서 HotSpot JVM에서는 객체의 생존 기간에 따라 Heap영역을 물리적으로 Young과 Old 총 2가지 영역으로 설계하였습니다.
- Young 영역(Yong Generation 영역):
- 새롭게 생성한 객체의 대부분이 여기에 위치한다. 대부분의 객체가 금방 접근 불가능 상태가 되기 때문에 매우 많은 객체가 Young 영역에 생성되었다가 사라진다.
- 이 영역에서 객체가 사라질 때 Minor GC가 발생한다고 말한다.
- Old 영역에 비해 상대적으로 작은 공간이기 때문에 메모리 상의 객체를 찾아 제거하는데 적은 시간이 걸리므로 Minor GC라 불린다.
- Old 영역(Old Generation 영역):
- 접근 불가능 상태로 되지 않아 Young 영역에서 살아남은 객체가 여기로 복사된다. 대부분 Young 영역보다 크게 할당하며, 크기가 큰 만큼 Young 영역보다 GC는 적게 발생한다.
- 이 영역에서 객체가 사라질 때 Major GC(혹은 Full GC)가 발생한다고 말한다.
Young 영역 및 Minor GC과정
Young 영역은 또다시 3개의 영역으로 나뉩니다.
- Eden 영역
- Survivor 영역(2개)
각 영역의 처리 절차를 순서는 다음과 같습니다.
- new를 통해 새로 생성한 객체는 Eden 영역에 위치한다.
- 객체가 계속 생성되어 Eden 영역이 꽉 차게 되고 Minor GC가 실행
- Mark 동작을 통해 reachable 객체를 탐색 후 Eden 영역에서 GC가 한 번 발생한 후 살아남은 객체는 Survivor 영역 중 하나로 이동하고 살아남은 모든 객체들은 age값이 1씩 증가
- Eden 영역에서 사용되지 않는 객체(unreachable)의 메모리를 해제(sweep)
- 하나의 Survivor 영역이 가득 차게 되면 그중에서 살아남은 객체를 다른 Survivor 영역으로 이동한다. 그리고 가득 찬 Survivor 영역은 아무 데이터도 없는 상태로 된다.
- 이 과정을 반복하다가 계속해서 살아남아 있는 객체는 Old 영역으로 이동하게 된다.
여기서 눈여겨볼 점은 Survivor 영역 중 하나는 반드시 비어 있는 상태로 남아 있어야 한다는 것입니다.
Old 영역 및 Major GC 과정
Old 영역은 기본적으로 데이터가 가득 차면 GC를 실행합니다. Major GC는 객체들이 계속 Promotion 되어 Old 영역의 메모리가 부족해지면 발생하게 됩니다.
1. Young 영역의 Survivor 영역에서 객체의 age가 임계값을 넘어가게 되면
2. 해당 객체를 Old영역으로 이동시키는데 이를 Promotion이라고 한다.
3. 1번과 2번의 반복된 과정으로 Old Generation 영역의 공간(메모리)이 꽉 차면 Major GC가 발생되게 된다.
이 과정에서 앞서 소개한 Stop-The-World 문제가 발생하게 됩니다.
Major GC가 일어나면 Thread가 멈추고 Mark and Sweep 작업을 하게 되는데, 이는 CPU에 부하를 주기 때문에 멈추는 현상이 발생하게 됩니다.
개발자들은 이를 조금이라도 해결하고자 GC알고리즘을 계속해서 발전시켜 왔습니다. (Old 영역에 대한 GC)
대표적인 GC 방식은 5가지 방식이 있으며 상황에 따라 필요한 GC 방식을 설정해서 사용할 수 있습니다.
GC 알고리즘 종류
- Serial GC
- Parallel GC
- Parallel Old GC (Parallel Compacting GC)
- Concurrent Mark & Sweep GC (이하 CMS)
- G1(Garbage First) GC
Serial GC
- 서버의 CPU 코어가 1개일 때 사용하기 위해 개발된 GC로 애플리케이션 성능이 많이 떨어져 운영 서버에서는 사용하면 안 된다.
- GC를 처리하는 스레드가 1개 (싱글 스레드)이고 가장 stop-the-world 시간이 길다
- Minor GC 에는 Mark-Sweep을 사용하고, Major GC에는 Mark-Sweep-Compact를 사용한다.
- Old영역에 살아있는 객체를 식별(Mark) → Unreachable 객체들을 Heap에서 제거(Sweep) → 각 객체들이 연속되게 쌓이도록 heap의 앞부분부터 채워서 객체가 존재하는 부분과 객체가 없는 부분으로 나눈다.(Compaction)
Parallel GC
- Java 8의 디폴트 GC
- Serial GC와 기본적인 알고리즘은 같지만, Young 영역의 Minor GC를 멀티 스레드로 수행 (Old 영역은 여전히 싱글 스레드) 하기 때문에 Serial GC보다 빠른 게 객체를 처리할 수 있다.
- Serial GC에 비해 stop-the-world 시간 감소
- 메모리가 충분하고 코어의 개수가 많을 때 유리하다.
Parallel Old GC (Parallel Compacting Collector)
- Parallel GC를 개선한 버전으로 Parallel GC와 비교하여 Old 영역의 GC 알고리즘만 다르다
- Young 영역뿐만 아니라, Old 영역에서도 멀티 스레드로 GC 수행
- Mark-Summary-Compact 방식을 이용
CMS GC (Concurrent Mark Sweep)
- 애플리케이션의 스레드와 GC 스레드가 동시에 실행되어 stop-the-world 시간이 매우 짧다는 장점
- 단점
- GC 과정이 매우 복잡해짐.
- 다른 GC 방식보다 메모리와 CPU를 더 많이 사용한다.
- Compaction 단계가 기본적으로 제공되지 않는다.
- CMS GC는 Java9 버전부터 deprecated 되었고 결국 Java14에서는 사용이 중지됨
G1 GC (Garbage First)
- Java 9+ 버전의 디폴트 GC로 지정
- G1은 Garbage First의 약어로 Garbage만 있는 Region을 먼저 회수한다고 해서 붙여진 이름이다.
- 대용량의 메모리가 있는 멀티 프로세서 시스템을 위해 제작됨
- 빠른 처리 속도를 달성하면서 일시 중지 시간(STW : Stop The World)의 최소화를 충족시키기는 것이 G1GC의 목표이나 STW를 완전히 없애지는 못한다.
- 기존의 GC 알고리즘들처럼 Heap영역을 Young/Old 영역으로 나누거나 일일이 메모리를 탐색하여 객체들을 제거하지 않는다.
- Region개념 도입, Heap메모리 전체 탐색이 아닌 영역(region)을 나눠 탐색하고 메모리가 많이 차 있는 영역을 우선적으로 GC 한다.
G1GC는 전체 heap을 체스판처럼 여러 영역(region)으로 나누어 관리합니다.
- 장점
- Heap 크기가 클수록 잘 동작한다.
- 처리 속도가 빠르다.
- Garbage로 가득 찬 영역을 빠르게 회수하여 빈 공간을 확보하므로, 결국 GC 빈도가 줄어드는 효과
- 단점
- 공간 부족 상태를 조심해야 한다. (Minor GC, Major GC 수행하고 나서도 여유 공간이 부족한 경우)
- 이때는 Full GC가 발생하는데, 이 GC는 Single Thread로 동작한다.
- Full GC는 heap 전반적으로 GC가 발생하는 것을 뜻한다.
- 작은 Heap 공간을 가지는 Application에서는 제 성능을 발휘하지 못하고 Full GC가 발생한다.
- 공간 부족 상태를 조심해야 한다. (Minor GC, Major GC 수행하고 나서도 여유 공간이 부족한 경우)
지금까지 GC에 대한 전반적인 내용을 요약해 보았습니다. 결국엔 STW의 시간을 줄이는 GC 튜닝이 필요한데, 이는 애플리케이션의 성격과 구조에 따라서 달라질 수 있기 때문에 각자의 비즈니스 히스토리를 파악해서 어떤 방법을 채택하는 것이 지속 가능한 설계인지 계속해서 고민해야 할 부분이라고 생각합니다.
아마 다음 GC 편이 나온다면 GC튜닝이나 메모리 누수, Heap dump 분석, APM을 주제로 해봐도 재미있을 것 같습니다. 프로젝트에 녹여볼 수도....
출처 및 같이 읽어 볼 포스팅
가비지컬렉터 GC 동작원리
트래픽이 많이 몰리는 이벤트가 예정되어 있을 때, Young Gen과 Old Gen의 비율 고민하기
[JVM] Garbage Collection Algorithms
Naver - Java Garbage Collection
널널한 개발자 - GC에서 파생될 수 있는 개념들과 이를 프로젝트에 대입해 볼 것
'Java' 카테고리의 다른 글
[동시성 톺아보기] Java Thread 주요 관리 API (sleep(), join(), interrupt()) (0) | 2024.03.25 |
---|---|
[동시성 톺아보기] Runnable 익명 클래스로 Thread 생성 (0) | 2024.03.17 |
[Java] instanceof 키워드 보단 다형성을 이용하자! (1) | 2023.12.04 |
[Java] equals()와 hashCode()를 같이 재정의 해야하는 이유 (0) | 2023.11.20 |
[Java] String을 초기화하는 방법 - 성능 비교 (0) | 2023.11.13 |