사용하지 않는 객체의 메모리를 GC(Garbage Collector)가 주기적으로 검사해서 청소해준다.
C와 C++ 같은 Unmanaged language는 free()와 같은 함수를 사용해서 메모리를 직접 메모리를 해제해야 한다.
이런 번거로운 일을 GC가 대신 해주고 있는 것이다.
사용하지 않는 객체의 메모리 점유는 결국 메모리 누수로 이어지게 된다.
메모리는 한정된 자원이기 때문에 사용하지 않거나 필요가 없는 부분은 해제를 해주는 것이 맞다.
가비지 컬렉션은 프로세스 자체를 얘기하고 컬렉터는 실제 역할을 수행하는 주체를 얘기한다.
JVM의 한 종류인 Hotspot JVM의 Heap 영역은 아래와 같이 생겼다고 한다.
가장 크게 비교하면 Young, Old, Permanent Generation으로 나뉘어져 있다.
각각 무엇인지 알아보자.
Perm 영역이 저장하는 정보들
Class 의 Meta정보
Method의 Meta 정보
Static Object
Class와 관련된 배열 객체 Meta 정보
JVM 내부적인 객체들과 최적화컴파일러(JIT)의 최적화 정보
Young과 Old는 객체의 생명주기와 연결 시켜서 이해하면 도움이 될 것 같다.
Perm 영역은 Hotspot JVM Heap 영역의 사진에 담겨 있어서 적었지만,
자바 8 버전 이후에는 metaspace 영역으로 대체 되었다고 한다.
대체된 이유를 알아보자.
위 JMV Heap 영역의 사진을 보면 Perm 영역이 없다.
대부분 JVM Heap 영역을 검색하면 위와 같은 사진 아니면 Perm 영역이 포함된 사진이 있을 것이다.
위에도 작성 했듯이 자바 8 버전 이후에는 metaspace 영역으로 대체 되었기 때문이다.
자바 GC는 객체가 가비지인지 판별하기 위해서 Reachability라는 개념을 사용한다.
어떤 객체에 유효한 참조가 있으면 ‘Reachable’로, 없으면 ‘unreachable’로 구별한다.
두 가지의 구분에 관련해서 Naver D2에 상당히 정리가 잘 되어있다.
아래의 그림과 설명을 통해 이해가 쉽게 되었다.
위 사진은 오라클 HotSpot VM 기준에 런타임 데이터 영역이다.
(스레드가 차지하는 영역, 객체를 생성 및 보관하는 큰 힙, 클래스 정보가 차지하는 메서드 영역)
Object는 객체이고, 화살표는 참조를 나타낸다.
여기서 보면 알 수 있듯이 객체들의 참조는 사슬처럼 이루어져 있다.
이런 상황에서 유효한 참조 여부를 파악하려면 항상 최초의 참조가 있어야 하는데,
이를 객체 참조의 root set이라고 한다.
힙에 있는 객체들에 대한 참조는 다음 4가지 종류 중 하나이다.
이들 중 ‘힙 내의 다른 객체에 의한 참조’를 제외한 3 가지는 root set으로 reachability를 판가름하는 기준이 된다.
위 사진을 보고 트리 자료구조가 떠올랐다.
Object a = new Object();
Object b = new Object();
a.setReference(b);
b.setReference(a);
순환 참조의 예시로 위와 같이 코드를 작성했다.
순환 참조란 두 개 이상의 객체가 서로를 참조하는 상황을 말한다.
위와 같은 상황의 문제점은 순환 참조된 객체들은 root set이 불분명해지고,
메모리 누수를 야기하는 원인이 된다.
스프링에서의 두 객체의 서로 순환 참조 문제와 비슷한 개념 같다.
GC의 root set에 이들 객체가 계속 유지되어 메모리 누수(Memory Leak)를 야기한다.
GC가 가비지를 판별하는 기준을 알게 되었다.
이제는 가비지 컬렉션 과정에 대해서 알아보자.
GC를 실행하기 위해 JVM 애플리케이션 실행을 멈추는 것을 말한다.
GC가 가비지의 메모리를 해제하는 행동의 코스트가 생각보다 높다는 것을 알게되었다.
그러면 stop the world는 왜 발생하는 걸까?
GC가 실행되는 동안에는 모든 객체의 참조 관계를 추적하고,
유효한 객체들과 그렇지 않은 객체들을 식별하여 메모리를 회수해야 하기 때문이다.
객체의 참조 관계는 실행 중에도 추적할 수 있지만, 이 작업이 매우 느리기 떄문에
stop the world를 통해 일시적으로 애플리케이션을 멈추고 추적 작업을 수행한다.
설명 보다는 사진이 더 기억에 잘 남아서 다시 사진을 찾아봤다.
실행 범위와 영향 범위를 보게 되면 가장 눈에 띄는 것은 Minor GC와 Major GC이다.
위 2개의 개념을 이해하면 왜 이렇게 나눠 놨는지 더 이해하기 쉬울 것 같다.
그렇다면 Minor GC와 Major GC의 동작 과정으로 나눠서 봐보자.
객체가 새롭게 생성되면 Young 영역 중에서도 Eden 영역에 할당(Allocation)이 된다.
그리고 Eden 영역이 꽉 차면 Minor GC가 발생하게 된다.
Young 영역의 동작 순서
- 새로 생성된 객체가 Eden 영역에 할당된다.
- 객체가 계속 생성되어 Eden 영역이 꽉차게 되고 Minor GC가 실행된다.
- Eden 영역에서 사용되지 않는 객체의 메모리가 해제된다.
- Eden 영역에서 살아남은 객체는 1개의 Survivor 영역으로 이동된다.
- 1~2번의 과정이 반복되다가 Survivor 영역이 가득 차게 되면 Survivor 영역의 살아남은 객체를 다른 Survivor 영역으로 이동시킨다.(1개의 Survivor 영역은 반드시 빈 상태가 된다.)
- 이러한 과정을 반복하여 계속해서 살아남은 객체는 Old 영역으로 이동(Promotion)된다.
여기서 객체의 생존 횟수를 카운트하기 위해 Minor GC에서 객체가 살아남은 횟수를 의미하는 Age를
Object Header에 기록한다.
Young 영역에서 오래 살아남은 객체는 Old 영역으로 Promotion되는 것을 확인했다.
Major GC는 객체들이 계속 Promotion 되어 Old 영역의 메모리가 부족해지면 발생하게 된다.
위 글은 Generational GC의 과정을 나타내고 있다.
Old 영역에 데이터가 가득 차면 사용하는 GC 방식이 있다고 한다.
위의 알고라즘에 대한 자세한 내용은 아래 3 개의 링크를 참고해서 공부해볼 수 있다.
여기서 우리는 Mark and Sweep과 mark-sweep-compact 알고리즘을 볼 것이다.
위와 같이 큰 틀로 구분이 되기 때문에 두 개를 알아보기로 했다.
우선 마크 앤 스윕부터 알아보도록 하자.
Mark라는 단어의 뜻은 표시하다 라는 뜻이 있다.
Sweep은 쓸어내리다, 소멸의 뜻이 있다고 한다.
GC Root는 실행중인 스레드, 정적 변수, 로컬 변수, JNI 레퍼런스와 같은 것들이 될 수 있다.
위에서 나왔던 root set과 동일한 용어이다.
GC는 객체에 Mark를 하고 Mark가 되지 않은 객체의 메모리를 해제하게 된다.
결국에 Mark가 안된 객체는 Unreachable 객체이다.
이미 Reachability에서 다뤘던 내용이기 때문에 쉽게 이해할 수 있었다.
더 자세한 내용이 궁금하면 아래의 블로그를 참고해보자
Old 영역에는 대부분 큰 객체들이 저장되는데 Mark and Sweep 알고리즘을 사용하면
큰 객체들을 처리할 때 많은 시간과 메모리가 소비된다고 한다.
대신 Old 영역에서는 mark-sweep-compact 알고리즘이 사용된다.
사진으로 보니까 훨씬 이해가 편하고 마음이 편안해진다.
예전에 하드디스크 성능을 위해 디스크 조각 모음을 했던 것과 비슷한 것 같다.
알아본 내용들을 다이어그램으로 표시해 봤다.
각 공부한 키워드들의 연관성을 좀 더 가시적으로 보기 위해 정리해봤다.
내용을 전부 머릿속에 기억 하는 것은 한계가 있을 수 있으니,
키워드들만 봐도 서로의 연관성과 개념이 떠오르도록 나의 방식대로 정리해봤다.