장미정원
✨ GC 이해하기 본문
GC란?
GC(Garbage Collector)는 주기적으로 힙 메모리의 사용하지 않는 객체를 검사하여 청소해줍니다.
C, C++과 같은 언어들은 Unmanaged Language로 생성한 객체에 대한 메모리 헤제를 직접 해야합니다. 하지만 이렇게 매번 객체를 생성하고 그 객체를 직접 해제하는 작업은 매우 번거롭고 휴먼에러가 발생할 가능성이 커집니다. (이중해제, 해제된 메모리 접근, 메모리 누수)
자바에서는 이런 메모리 해제 작업을 GC가 대신 해줍니다. 사용하지 않는 객체의 메모리가 해제되지 않는다면 메모리 누수로 이어지고 OOM에러가 발생해 애플리케이션이 다운될 수도 있습니다. 메모리는 한정된 자원이기 때문에 사용하지 않는 부분은 해제를 해주어야합니다.
JVM Heap
JVM의 메모리는 크게 Method Area, Stack, Heap, Native Method Stack이렇게 4개의 영역으로 나뉩니다. 가비지 컬렉터는 이 중 Heap 메모리를 대상으로 GC를 수행합니다. 이 Heap 영역에는 new 키워드로 생성한 객체들이 적재되는 영역입니다.
JVM의 힙 영역은 이렇게 생겼습니다. 크게 Young, Old, Permanent Generation으로 구분되어 있습니다. 참고로 Permanent Generation 영역은 자바8부터 삭제되고 metaspace 영역으로 대체되었습니다.
Young Generation
- 새롭게 생성된 객체가 적재되는 영역입니다. 거의 대부분의 객체들은 금방 Unreachable 상태, 즉 GC의 대상이 되기 때문에 Young 영역에서 생존하다 사라집니다.
- 이 Young 영역에 대한 GC를 Minor GC라고 합니다.
Old Generation
- Young 영역에서 Reachable 상태를 유지해서 살아남은 객체가 존재하는 영역입니다. Young 영역보다 큰 영역이며 큰 만틈 GC는 적게 발생합니다.
- Old 영역에 대한 GC를 Major GC, Full GC라고 합니다.
Permanent Generation
JVM에서 클래스의 메타데이터가 저장되는 영역입니다. 클래스 로더는 클래스를 읽어들인 후 해당 영역에 클래스의 메타데이터를 적재합니다.
Permanent Generation이 삭제된 이유
자바8부터는 Permanent Generation 영역이 사라지고 MetaSpace 영역으로 대체되었습니다.
Metaspace?
Permanent 영역에서 저장하는 정보들인 클래스의 메타데이터들이 Metaspace에 저장됩니다.
Permanent Generation은 Heap 영역에 위치했지만 Metaspace는 Native 메모리 영역에 위치합니다. Heap 영역은 JVM이 관리하지만 Native 메모리 영역은 OS 레벨에서 관리됩니다. 이렇게 대체된 이유는 OOM(OutOfMemoryError)와 밀접한 연관이 있습니다.
기존 Permanent 영역은 JVM의 Heap 영역에 위치했습니다. 그렇기 때문에 Deafult로 생성되는 사이즈가 굉장히 작았습니다. 저장되야하는 데이터는 많은데 굉장히 작은 값을 Default 사이즈로 가지고 있기 때문에 OOM 문제가 빈번하게 일어났습니다.
그렇기 때문에 Heap 메모리에서 해당 데이터를 적재하지 않고 Native 메모리에서 적재하도록 하여 JVM이 아닌 OS가 관리하도록 변경하였습니다. OS가 메모리를 관리하게 됨에 따라 유동적으로 메모리를 관리하게 됩니다. (Default 값이 unlimited입니다.)
그렇다고 무작정 Heap 메모리의 크기를 늘려 튜닝한다면 GC를 진행하는데 아주 오랜 시간이 소모되며 GC를 수행할 때는 애플리케이션이 잠시 중단되기 때문에 성능이 오히려 좋지 않아지고 자칫하면 GC Overhead limit exceeded라는 OOM을 마주할 수도 있다.
Reachable? UnReachable?
GC의 대상이 되는 객체는 무엇일까요?
생성된 객체에 대한 참조가 존재한다면 Reachable 상태, 그렇지 않다면 UnReachable 상태인 객체라고 하며 UnReachable 상태의 객체는 GC의 수거 대상입니다.
GC는 Root Set과의 관계로 Reachable, UnReachable 상태를 구분합니다. Root Set과의 참조 관계의 여부를 파악하여 판단합니다.
Root Set은 Stack 영역의 지역변수, 파라미터, Method 영역의 정적 변수, JNI에 의해 생성된 객체입니다.
GC의 동작 과정과 알고리즘
먼저 GC는 Root Set으로부터 Heap 영역의 모든 객체를 스캔하여 Reachable한 객체를 찾습니다(Mark).
그리고 Mark가 되지 않은 Unreachable한 객체를 제거합니다. (Sweep)
이렇게 GC는 Mark와 Sweep이라는 동작을 통해 진행되고, 이 GC 알고리즘을 Mark and Sweep 알고리즘이라고 합니다. JVM은 GC 실행 기준을 설정하여 특정 기준을 충족할 때마다 GC를 발생시킵니다.
힙 영역을 나눈 이유?
위에서 힙 영역을 나뉜것에 대해 설명하였습니다. 위에서 설명했다시피 대부분의 객체는 금방 Unreachable 상태가 되어 GC 대상이 됩니다.
따라서 힙의 영역이 나뉘지 않았다면 모든 객체들에 대해 GC를 진행하여 비효율적입니다. 그렇기 때문에 오래된 객체들은 따로 빼두고 (Old Generation) 최근 생성된 객체들만 (Young Generation) 주기적으로 스캔하는 것이 효율적이기 때문에 힙 영역의 Generation이 나뉘었습니다!
Minor GC
먼저 Minor GC에 대해 알아보겠습니다. Minor GC는 Young Generation 영역에 대한 GC입니다. 위에서 설명했다시피 Young Generation의 객체들의 대부분은 금방 GC의 대상이 되기 때문에 Minor GC는 주기적으로 일어납니다.
- 객체가 처음 생성되면 Eden이라는 곳이 적재됩니다. 여기서 Eden 영역이 꽉차게 된다면 Minor GC가 실행됩니다.
- 그후 GC는 mark and sweep 알고리즘으로 객체들을 스캔 후 Reachable한 객체들은 Survivor 0 영역으로 옮겨집니다. 이때 옮겨지면서 age-bit 값이 1 올라갑니다.
- Eden 영역이 또 꽉차게된다면 Minor GC가 실행되고 Reachable한 객체들은 Survivor 1 영역, Survivor 0 영역의 객체들도 Survivor 1 영역으로 이동합니다. (age-bit 증가)
- Survivor 0, Survivor 1 영역을 왔다 갔다 하면서 객체는 생존하게 되고 age-bit가 특정값만큼 올라간다면 Old Generation으로 옮겨집니다.
Full GC, Major GC
Old Generation에 대한 GC입니다. Full GC는 Minor GC보다 오래걸립니다. (영역이 크기 때문입니다.) 동작 방식은 동일하며 Young Generation과 마찬가지로 Old Generation이 꽉찬다면 Full GC를 진행합니다.
Stop the World
GC를 진행할 때는 애플리케이션이 잠시 멈추는 것을 Stop the World라고 합니다. GC가 발생하면 GC를 실행하는 쓰레드를 제외한 모든 쓰레드는 작업을 멈추고 기다립니다.
GC가 실행되는 동안에는 모든 객체의 참조 관계를 추적합니다. 메모리의 일관성과 안정성을 위해 애플리케이션 작업을 일시중지합니다.
GC 방식
이렇게 GC의 동작중 발생하는 Stop the World의 시간을 짧게 가져가는 것이 곧 애플리케이션 최적화라고 할 수 있습니다. JVM은 Stop the World의 시간을 최적화 하기 위해 여러 GC 방식들이 있습니다.
Parallel GC
java8 부터 기본적으로 쓰이는 GC방식입니다. 멀티코어 환경에서 주로 사용되며 여러 쓰레드로 GC를 실행하기 때문에 Stop the World의 시간이 짧습니다. Old 영역에서는 싱글쓰레드, Young 영역에서는 멀티 쓰레드로 실행됩니다.
G1 GC
java9 부터 기본적으로 쓰이는 GC입니다. 기존 힙 영역을 구분하는 방식과는 다르게 힙 영역을 region으로 잘게 나누어 생존하는 객체가 적은 region부터 GC하는 방식입니다. 이 region의 개수를 알아서 튜닝을 해주기 때문에 Stop the World 시간을 최적화할 수 있습니다.
참고자료
'Back-end' 카테고리의 다른 글
[DB] 인덱스 진짜 이해하기 (1) | 2024.08.19 |
---|---|
✨ 스프링 AOP (0) | 2024.08.10 |
✨ 스프링을 사용하는 이유 (0) | 2024.07.29 |
✨ JDBC와 DataSource (0) | 2024.07.01 |
교내 프로젝트 운영중 발생한 동시성 문제와 IntegerOverFlow 문제 해결기 (0) | 2024.06.16 |