JVM의 실행엔진에 속한 GC(Garbage Collector)는 개발자가 직접 메모리를 관리해주지 않아도 자동으로 메모리를 관리해주는 기술입니다. 어떻게 메모리를 관리해주는지에 대해 알아보겠습니다.
GC는 앞서 말했듯이, 개발자가 직접 메모리를 관리해주지 않아도 자동으로 메모리를 관리해주는 기술입니다. 그렇기에 C/C++과는 달리, Java에서는 메모리 누수(memory leak)에 대해 걱정할 필요가 없습니다. 어플리케이션의 규모가 커질수록 메모리를 직접 할당하고 해제하는 작업은 굉장히 복잡해지게 됩니다.
하지만, 언제나 등장하는 there is no silver bullet
명언... 즉, GC에도 단점은 존재합니다. 개발자는 GC가 언제 동작하는지 자세히 알 수 없고, 어플리케이션 실행 중 GC 스레드가 실행되어 어플리케이션이 중단될 수 있습니다.
정리
- 장점
- 메모리 누수(memory leak)에 대해 걱정할 필요가 없다
- 단점
- 개발자는 GC가 언제 동작하는지 자세히 알 수 없다
- 어플리케이션 실행 중 GC 스레드가 실행되어 어플리케이션이 중단될 수 있다
GC는 내부적으로 다양한 알고리즘들을 활용해서 힙 영역 내부에 필요없는 객체들이 차지하고 있는 메모리를 회수합니다.
Mark and Sweep
알고리즘은 객체들을 reachable
상태와 unreachable
상태로 구분해서, reachable
상태의 객체들은 냅두고, unreachable
상태의 객체들은 차지하고 있는 메모리를 회수합니다.
위의 그림을 보면, 객체들이 Root space(혹은 Root set)에서 참조되고 있으면 reachable object
라고 하고, 참조되고 있지 않다면 Unreachable objects
라고 합니다. 참조되고 있다면 reference count
가 1 증가합니다.
reachable
객체reference count
가 0이 아닌 객체 unreachable
객체reference count
가 0인 객체그리고 GC의 대상은 Unreachable Object
들이 되어, GC 동작 시에 Unreachable object 들이 차지하고 있던 메모리가 회수됩니다.
Mark and Sweep
이라는 이름이 붙은 이유는 참조되고 있다면 마킹(Mark)해두고, 마킹되지 않은 객체들을 삭제(Sweep) 한다고 해서 mark and sweep 알고리즘 인 것입니다. 아래의 그림을 보면 이해가 가능합니다.
reachable 상태
에 대해 조금 더 깊게 들어가면, 총 4가지로 나누기도 합니다. weakly reachable 상태의 객체들은 GC의 대상이 됩니다. 해당 내용에 대해 더 궁금하다면 해당포스팅 을 참고해주세요! :)
- strongly reachable
- softly reachable
- phantomly reachable
- weakly reachable
Root space
는 힙 영역에 있는 객체들을 참조하고 있는 메모리 영역의 집합을 일컫습니다. 그리고 힙 영역에 있는 객체들에 대한 참조는 다음 4가지 종류 중 하나입니다.
순환참조
)Java Stack
, 즉 Java 메서드 실행 시에 사용하는 지역 변수와 파라미터들에 의한 참조Native Stack
, 즉 JNI(Java Native Interface)에 의해 생성된 객체에 대한 참조메서드 영역의 정적 변수
에 의한 참조위의 4가지 중 순환참조(힙 내의 다른 객체에 의한 참조)는 외부에서 참조하는 것이 아니기에 reachable vs unreachable 상태를 판단짓는 용도로 사용되지 않습니다. 즉, Root space 에서 참조되고 있느냐는 나머지 3가지의 참조에 대해서 결정됩니다.
mark and sweep
알고리즘은 mark and sweep
알고리즘에 대한 그림에서도 볼 수 있다시피 메모리가 Fragmentation, 단편화되는 단점이 있습니다. 메모리에서의 단편화는 정렬되지 않은 조각으로 나뉘어져, 절대적인 크기는 충분하지만 추가적으로 메모리 할당이 되기 힘든 상태를 의미합니다. 이를 해결하기 위한 것이 바로 Mark-Sweep-Compact 알고리즘
입니다. 많은 GC 알고리즘들이 GC를 구현하는데에 있어서Mark-Sweep-Compact 알고리즘
을 사용합니다.
메모리 단편화(Memory Fragmentation)란?
메모리 단편화란, RAM에서 메모리의 공간이 작은 조각으로 나뉘어져 사용가능한 메모리가 충분히 존재하지만 할당(사용)이 불가능한 상태를 의미한다.
그리고 메모리 단편화에는 내부 단편화와 외부 단편화로 나뉘는데, 위 상황에서의 메모리 단편화는 외부 단편화를 의미합니다.
- 내부 단편화 : 프로세스가 필요한 양보다 더 큰 메모리가 할당되어서 프로세스에서 사용하는 메모리 공간이 낭비 되는 상황
- 외부 단편화 : 중간중간에 생긴 사용하지 않는 메모리가 많이 존재해서 총 메모리 공간은 충분하지만 실제로 할당할 수 없는 상황
Mark-Sweep-Compact
알고리즘도 Mark And Sweep
알고리즘과 마찬가지로 참조되는 객체들에 대해서 마크를 하고, 참조되지 않으면 삭제를 합니다. 대신 Mark And Compact
알고리즘은 이후에 메모리를 정리하여, 메모리 단편화를 해결한다는 차이점이 있습니다.
GC에 대해 이해하려면, Stop-the-world 에 대해 이해해야 합니다. GC가 동작하기 위해 GC 스레드가 실행될 때에는 어플리케이션과 관련된 다른 스레드는 실행되지 않습니다. 이것을 Stop-the-world
라고 합니다.
GC가 동작함으로써 JVM의 메모리를 확보할 수 있다는 장점도 있지만 단점이 너무나도 명확합니다. 다른 스레드들이 동작하지 못하게 돼 프로그램의 성능에 저하를 준다는 점입니다.
앞에서 배운 Minor GC와 Major GC 모두 실행될 때에는 stop-the-world
가 발생해 다른 모든 스레드들은 중지됩니다. 하지만, stop-the-world
가 발생해도 Minor GC는 상대적으로 작은 Young 영역에 대해 GC가 일어나기에 매우 짧지만, Major GC(혹은 Full GC)의 경우에는 비교적 큰 Old 영역에 대해 GC가 발생하기에 GC가 오랜 시간동안 지속됩니다.
JVM의 힙 영역은 GC가 효율적으로 동작할 수 있게끔 힙 영역이 세분화되어 있습니다. 크게는 Young
, Old
로 나눌 수 있고, Young 영역에는 Eden
, Survivor0
, Survivor1
영역으로 나뉜다. 아래 그림과 같습니다.
무슨 이유로 힙 영역을 세분화시키는가?
힙 영역을 세분화시킨 데에는 이유가 있을 터... 무슨 이유일까? GC를 만들고 힙 영역을 세분화할 때는
weak generational hypothesis
라는 가설을 토대로 만들어 졌다고 합니다. 가설의 내용은 아래와 같습니다.
- 대부분의 객체는 금방 접근 불가능 상태(unreachable)가 된다.
- 오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다.
그래서 JVM의 힙 영역을 세분화시킴으로써 GC의 효율을 극대화시킨 것입니다.
그래서 Young 영역은 Old 영역의 사이즈보다 작게 유지한채, 대부분의 객체들이 Young 영역에서 메모리가 회수되도록 구현되어 있습니다.
다만 몇몇 객체들은 Young 영역에서 계속 살아남아 Old 영역으로 복사되는데 이 과정을 aging
이라고 합니다.
age
를 임계값으로 두고, 특정 age
가 되면 Old 영역으로 복사됩니다. 그리고 Young 영역에서 발생한 GC는 Minor GC
라 부르고, Old 영역에서 발생한 GC는 Major GC(Full GC)
라고 부릅니다.
Minor GC
는 Young 영역에서 발생하는 GC를 말합니다.
Minor GC는 Major GC에 비해 작은 메모리 영역에 대해 GC가 동작하기에 비교적 빠른 시간 내(50ms 내외)에 GC 작업이 마무리됩니다.
Eden 영역
에 위치한다.Eden 영역
에서 GC가 한 번 발생한 후 살아남은 객체는 Survivor 영역
중 하나로 이동된다.Eden 영역
에서 GC가 발생하면 이미 살아남은 객체가 존재하는 Survivor 영역
으로 객체가 계속 쌓인다.하나의 Survivor 영역
이 가득 차게 되면 그 중에서 살아남은 객체를 다른 Survivor 영역
으로 이동한다. 그리고 가득 찬 Survivor 영역은 아무 데이터도 없는 상태로 된다.계속해서 살아남아 있는 객체
는 Old 영역
으로 이동하게 된다.Major GC(혹은 Full GC)
는 Old 영역에서 발생하는 GC를 말합니다. 즉, Major GC
는 Old 영역에 있는 객체들에 대해 GC를 수행하는 것을 말합니다.
Old 영역은 Young 영역에 비해 보다 큰 메모리 사이즈를 가지므로, Major GC
는 비교적 오랜 시간이 걸리게 됩니다. 그래서 일반적으로 GC로 인한 성능 저하를 막기 위해서는 Major GC
의 stop-the-world
시간을 줄이거나, 빈도수를 줄이는 방법을 고안합니다.
앞에서 GC는 다양한 알고리즘들로 구현될 수 있다고 얘기했습니다. 이제 GC의 종류들에 대해 알아보도록 하겠습니다.
Serial GC에서 Young 영역에 대한 GC와 Old 영역에 대한 GC가 어떻게 이뤄지는지 알아보자. Serial GC에서 Young 영역에 대한 GC는 bump-the-pointer
와 TLABs
라는 기술을 통해 구현되어 있습니다.
bump-the-pointer
와TLABs
에 대한 내용은 해당 포스팅을 참고하자!
Old 영역의 GC는 앞에서 봤던 mark-sweep-compact
이라는 알고리즘을 사용합니다. 이 알고리즘의 첫 단계는 Old 영역에 살아 있는 객체를 식별(Mark)하는 것입니다. 그 다음에는 힙(heap)의 앞 부분부터 확인하여 살아 있는 것만 남긴 뒤(Sweep), 마지막 단계에서는 각 객체들이 연속되게 쌓이도록 힙의 가장 앞 부분부터 채워서 객체가 존재하는 부분과 객체가 없는 부분으로 나눕니다(Compaction).
단, Serial GC는 GC를 처리하는 스레드가 1개이므로, stop-the-world가 길어질 수 밖에 없습니다. 그렇기에 Serial GC는 적은 메모리와 CPU 코어 개수가 적을 때 적합한 방식이라고 볼 수 있습니다.
Parallel GC
는 Serial GC
와 같은 알고리즘으로 동작합니다. 다만, Parallel GC
는 GC를 처리하는 스레드가 여러 개이고, Serial GC는 GC를 처리하는 스레드가 1개라는 차이점이 있습니다. 즉, Parallel GC는 멀티 스레드 환경에서 GC가 동작하고, Serial GC는 단일 스레드 환경에서 GC가 동작합니다. 그렇기에 Parallel GC
는 메모리가 충분하고 코어의 개수가 많을 때 유리하다. Parallel GC는 Throughput GC
라고도 부른다.
Parallel GC
는 멀티 스레드에서 GC가 동작하기에, Serial GC에 비해 stop-the-world를 훨씬 단축시켰습니다. 또한, Parallel GC
는java 8에서 default gc로 사용되는 GC 알고리즘입니다.
CMS GC
는 앞선 GC들에 비해 조금 더 복잡하게 동작합니다. 아래 그림은 Serial GC
와 CMS GC
의 동작 과정을 비교하는 그림입니다.
초기 Initial Mark
단계에서는 클래스 로더에서 가장 가까운 객체 중 살아 있는 객체만 찾는 것으로 끝냅니다. 따라서, 멈추는 시간은 매우 짧습니다. 그리고 Concurrent Mark
단계에서는 방금 살아있다고 확인한 객체에서 참조하고 있는 객체들을 따라가면서 확인합니다. 이 단계의 특징은 다른 스레드가 실행 중인 상태에서 동시에 진행된다는 것입니다.
그 다음 Remark
단계에서는 Concurrent Mark
단계에서 새로 추가되거나 참조가 끊긴 객체를 확인합니다. 마지막으로 Concurrent Sweep
단계에서는 쓰레기를 정리하는 작업을 실행합니다. 이 작업도 다른 스레드가 실행되고 있는 상황에서 진행됩니다.
이러한 단계로 진행되는 GC 방식이기 때문에 stop-the-world
시간이 매우 짧다는 특징을 가집니다. 모든 애플리케이션의 응답 속도가 매우 중요할 때 CMS GC
를 사용하며, Low Latency GC
라고도 부른다.
그런데 CMS GC
는 stop-the-world 시간이 짧다는 장점에 반해 다음과 같은 단점이 존재한다.
Compaction
작업을 실행하면 다른 GC 방식의 stop-the-world
시간보다 stop-the-world
시간이 더 길기 때문에 Compaction 작업이 얼마나 자주, 오랫동안 수행되는지 확인해야 합니다.따라서, CMS GC를 사용할 때에는 신중히 검토한 후에 사용해야 합니다.
G1 GC
는 Heap 영역을 region 별로 구분해서 Heap 영역을 바둑판처럼 만들어놓습니다. Heap 영역에 저장되는 객체들은 처음에 Young 영역
으로 구분되는 region에 할당되고, GC가 동작함에도 살아남는다면, Old 영역
으로 구분되는 region에 저장됩니다.
아래 그림은 Java 12 공식문서에서 G1 GC가 Heap 영역을 관리하는 레이아웃 구조를 나타냅니다.
humongous objects
) 들을 저장하는 region)https://d2.naver.com/helloworld/329631
https://d2.naver.com/helloworld/1329
https://jaemunbro.medium.com/java-metaspace%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90-ac363816d35e
https://tecoble.techcourse.co.kr/post/2021-08-30-jvm-gc/
https://jeong-pro.tistory.com/91