C/C++ 프로그래밍을 할 때 메모리 누수를 막기 위해 객체를 생성한 후 사용하지 않는 객체의 메모리를 직접 해제해줘야 했다. 하지만 Java에서는 JVM의 가비지 컬렉션(GC)이 불필요한 메모리를 알아서 정리해주기 때문에 직접 해제해주는 일이 없다.
GC를 해도 더이상 사용 가능한 메모리 영역이 없는데 계속 메모리 할당을 하려고 하면, OutOfMemoryError 가 발생하여 WAS가 다운될 수도 있다.
가바지 컬렉션이란?
사용하지 않는 객체를 메모리에서 삭제하는 작업을 GC(Garbage Collection)라고 하고, JVM에서 GC를 수행한다.
JVM의 메모리는 5가지 영역(class, stack, heap, native method, PC)으로 나뉘는데, GC는 힙 메모리만 다룬다.
일반적으로 다음과 같은 경우에 GC의 대상이 된다.
- 객체가 NULL인 경우
- 블럭 실행 종료 후, 블럭 안에서 생성된 객체
- 부모 객체가 NULL인 경우, 포함하는 자식 객체
GC의 메모리 해제 과정
1) Marking
프로세스는 마킹을 호출한다. 이것은 GC가 메모리의 사용 여부를 확인한다. 참조되는 객체는 파란색, 참조되지 않는 객체는 주황색으로 보여진다. 마킹 단계에서 모든 객체는 결정을 위해 스캔되는데, 이로 인해 많은 시간을 소모한다.
2) Normal Deletion
참조되징 않는 객체를 제거하고 메모리를 반환한다. 메모리 Allocator는 반환되어 비어진 블럭의 참조 위치를 저장해 두고, 새로운 오브젝트가 선언되면 할당되도록 한다.
3) Compacting
수행을 향상시키기 위해 참조되지 않는 객체를 제거하고, 남은 참조되는 객체들을 묶는다. 이들을 묶음으로서 공간이 생기므로 새로운 메모리 할당 시에 더 쉽고 빠르게 진행할 수 있다.
Weak Generational Hypothesis
JVM의 힙 영역은 다음의 2가지 가설(Weak Generational Hypothesis)에 기반하여 설계되었다.
- 대부분의 객체는 생성된 이후 짧은 시간안에 접근 불가능한 상태(Unreachable)가 된다.
- 오래된 객체에서 신규 객체로의 참조는 아주 적게 존재한다.
즉, 객체는 대부분 일회성이며 메모리에 오랫동안 남아있는 경우는 드물다는 것이다. 이 가설에 기반하여 Java는 메모리를 객체의 생존 기간에 따라 물리적인 힙 영역으로 나누게 되었고, Young 영역과 Old 영역으로 분할한다. 신규로 생성된 객체는 Young 영역에, 오랫동안 살아남은 객체는 Old 영역에 보관한다.
1) Young 영역 (Young Generation)
- 새롭게 생성된 객체가 할당되는 영역
- 대부분의 객체가 금방 Unreachable 상태가 되기 때문에, 많은 객체가 Young 영역에 생성되었다가 사라진다.
- 이 영역에 대한 GC를 Minor GC 라고 한다.
Young 영역은 1개의 Eden 영역과 2개의 Survivor 영역으로 나뉜다.
- Eden 영역 : 새로 생성된 객체가 할당되는 영역
- Survivor 영역 : 최소 1번의 GC 이후 살아남은 객체가 존재하는 영역
2) Old 영역 (Old Generation)
- Young 영역에서 Reachable 상태를 유지하여 살아남은 객체가 복사되는 영역
- Young 영역보다 크게 할당되며 영역의 크기가 큰 만큼 GC가 적게 발생한다.
- 이 영역에 대한 GC를 `Major GC` (또는 `Full GC`) 라고 한다.
3) Permanent 영역 (Permanent Generation)
- Method Area라고도 한다.
- JVM이 클래스들과 메소드들을 설명하기 위해 필요한 메타 데이터들을 포함한다.
- JDK 8부터 이 영역은 Metaspace로 교체된다.
Old 영역이 Young 영역보다 크게 할당되는 이유는 Young 영역의 수명이 짧은 객체들은 큰 공간을 필요로 하지 않으며 큰 객체들은 Old 영역에 할당되기 때문이다.
예외적인 상황으로 Old 영역에 있는 객체가 Young 영역의 객체를 참조할 경우가 있을 것이다. 이러한 경우를 대비하여 Old 영역에는 512 bytes의 덩어리(Chunk)로 되어있는 카드 테이블이 존재한다.
카드 테이블에는 Old 영역에 있는 객체가 Young 영역의 객체를 참조할 때마다 그에 대한 정보가 표시된다. Young 영역에서 GC가 실행될 때 Old 영역에 존재하는 모든 객체를 검사하여 참조되지 않는 Young 영역의 객체를 식별하는 것이 비효율적이기 때문이다. 그렇기 때문에 Young 영역에서 GC가 진행될 때, 카드 테이블만 조회하여 GC의 대상인지 식별할 수 있도록 한다.
객체의 Promotion 과정
Young 영역에서 Old 영역으로 객체가 이동하는 행위를 프로모션(Promotion)이라고 한다.
1) 새로운 객체가 들어오면 Eden 영역에 할당된다.
2) Eden 영역이 가득차게 되면 Minor GC가 시작된다.
3) 참조되는 객체들은 첫번째 Survivor(S0) 영역으로 이동되고 age 값을 1증가시킨다. 비참조 객체의 메모리 공간은 GC에 의해 회수된다.
4) 몇 번의 Minor GC가 발생하면서 살아남은 객체들이 S0 영역에 채워딘다. S0 영역에 가득찼을 때, S0 영역에 있는 객체에 대해서도 가비지 여부를 판단해서 회수할 수 있으면 회수한다. 이때 S0 영역에서 살아남은 객체들은 age 값이 증가되고 S1 영역으로 옮겨진다.
5) 결국 계속 살아남은 객체들은 S0과 S1 영역을 오가면서 age 값이 올라가게 된다.
6) 이 과정을 반복하다가 일정 수준 이상(JVM에서 설정해놓은 값)으로 age 값에 도달하면 객체를 Old 영역으로 이동시킨다. 객체가 promotion 된 것이다.
7) 시간이 지나 Old 영역에 할당된 메모리가 허용치를 넘게 되면, Old 영역에 있는 모든 객체들을 검사하여 참조되지 않는 객체들을 한꺼번에 삭제하는 GC가 실행된다. 이렇게 Old 영역에 있는 메모리를 회수하는 GC를 Major GC라고 한다. Major GC는 시간이 오래 걸리는 작업이고, GC를 실행하는 스레드를 제외한 모든 스레드는 작업을 멈춘다. 이를 Stop-the-World 라고 하며 이 작업이 너무 잦으면 프로그램 성능에 문제가 될 수 있다.
Reference
'Backend Roadmap > Java' 카테고리의 다른 글
[Java] final 키워드 (0) | 2024.09.03 |
---|---|
정적 바인딩과 동적 바인딩 (0) | 2024.06.25 |
오토 박싱과 오토 언박싱 (0) | 2024.06.25 |
스트림(Stream) (0) | 2024.06.25 |
자바 타입 제네릭(Generic) (0) | 2024.06.25 |