C++과 같이 개발자가 직접 메모리 관리를 하지 않고, 언어레벨에서 사용하지 않는 메모리는 “알아서” 관리해주는 JVM 구성 요소이다.
메모리 관리를 위한 기본 컨셉은 Weak generational hypothesis 이다.
대부분의 객체는 금방 접근 불가능한 상태(unreachable)가 된다.
오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다.
그래서 JVM에서 크게 2개의 물리적 공간으로 나누었다. Young 영역과 Old 영역이다.
새롭게 생성한 객체의 대부분이 여기에 위치한다. 대부분의 객체가 금방 접근 불가능 상태가 되기 때문에 매우 많은 객체가 Young 영역에 생성되었다가 사라진다. 이 영역에서 객체가 사라질때 Minor GC가 발생한다고 말한다.
접근 불가능 상태로 되지 않아 Young 영역에서 살아남은 객체가 여기로 복사된다. 대부분 Young 영역보다 크게 할당하며, 크기가 큰 만큼 Young 영역보다 GC는 적게 발생한다. 이 영역에서 객체가 사라질 때 Major GC(혹은 Full GC)가 발생한다고 말한다.
- MinorGC : Young Generation 에서 발생하는 GC
- MajorGC : Old Generation (Tenured Space) 에서 발생하는 GC
- FullGC : Heap 전체를 GC 하는 작업 (Young/Old 공간 모두)
Eden 영역이 꽉 차면 Minor GC가 발생하게 되는데, 사용되지 않는 메모리는 해제되고 Eden 영역에 존재하는 객체는 (사용중인) Survivor 영역으로 옮겨지게 된다.
Survivor 영역은 총 2개이지만 반드시 1개의 영역에만 데이터가 존재해야 한다.
새로운 오브젝트는 Eden 영역에 할당된다. 두 개의 Survivor Space 는 비워진 상태로 시작한다.
Eden 영역이 가득차면, MinorGC 가 발생한다.
MinorGC 가 발생하면, Reachable 오브젝트들은 Survivor1 으로 옮겨진다. Unreachable 오브젝트들은 Eden 영역이 클리어 될때 함께 메모리에서 사라진다.
다음 MinorGC 가 발생할때, Eden 영역에는 3번과 같은 과정이 발생한다. Unreachable 오브젝트들은 지워지고, Reachable 오브젝트들은 Survivor Space 로 이동한다. 기존에 Survivor1 에 있었던 Reachable 오브젝트들은 Survivor2 로 옮겨지는데, 이때, age 값이 증가되어 옮겨진다. 살아남은 모든 오브젝트들이 Survivor2 으로 모두 옮겨지면, Survivor1 과 Eden 은 클리어 된다. Survivor Space 에서 Survivor Space 로의 이동은 이동할때마다 age 값이 증가한다. 1개의 Survivor 영역은 반드시 빈 상태가 된다.
다음 MinorGC 가 발생하면, 4번 과정이 반복되는데, Survivor2 가 가득차 있었으므로 Survivor2 에서 살아남은 오브젝트들은 Survivor1 으로 옮겨지면서 Eden 과 Survivor2 는 클리어 된다. 이때에도, age 값이 증가되어 옮겨진다.
Young Generation 에서 계속해서 살아남으며 age 값이 증가하는 오브젝트들은 age 값이 특정값 이상이 되면 Old Generation 으로 옮겨지는데 이 단계를 Promotion 이라고 한다.
MinorGC 가 계속해서 반복되면, Promotion 작업도 꾸준히 발생하게 된다.
Promotion 작업이 계속해서 반복되면서 Old Generation 이 가득차게 되면 MajorGC 가 발생하게 된다.
초기 알고리즘으로 대표적으로 Reference Counting Algorithm과 Mark-and-Sweep Algorithm이 있다. 추가로 몇가지 알고리즘을 더 다루어 보도록 하자.
Tri-color Marking은 나중에 다루어 보도록 하자.
GC 사이클이 발생하여 Garbage 수집하는 동안 모든 어플리게이션 스레드가 중단된다. GC 스레드 외에는 모두 작업을 멈춘다. 모든 GC 알고리즘은 STW를 한다. 대개의 경우 Garbage Collector 튜닝은 STW 시간을 줄이는 것이다.
Garbage란 자신을 참조하는 곳이 아무데도 없으면 Garbage이다. 이것을 찾기위해 자신이 자신의 참조값을 들고 있어, 만약 참조값이 0이면 자신을 참조하는 곳이 아무데도 없다는 의미이다. 문제는 순환참조를 잡아내지 못한다는 것이다.
Reference Counting Algorithm의 단점을 극복하기 위해 나온 Algorithm이다. Root Set에서부터 Reference의 관계를 추적하여 Garbage를 찾는 것이다. 그래서 Tracing Algorithm이라고도 불린다.
GC Root
GC Root는 메모리의 Anchor Point로 메모리 풀 외부에서 내부를 가리키는 포인터이다.
- 실행중인 쓰레드 (Active Thread)
- 정적 변수 (Static Variable)
- 로컬 변수 (Local Variable)
- JNI 레퍼런스 (JNI Reference)
Root Set에서 도달할 수 있는 객체는 Mark하고 그렇지 못한 객체는 Sweep과정으로 free하는 것이다.
Mark and sweep 만하면 메모리가 듬성듬성 사용하는 형태가 된다. 이럴경우 메모리가 있음에도 메모리 할당이 불가능한 상황이 생길 수 있다. 이것을 조각 모음할 필요가 있다.
Mark and Sweep and Compact Algorithm이다. 살아남은 객체들이 연속된 메모리 공간으로 몰아 넣는 것이다. 메모리 공간이 변하기 때문에 Referece 업데이트 과정이 생겨 Overhead가 있다.
Survivor Space에서 쓰는 알고리즘으로, Mark-and-Copy Algorithm이라고도한다. Mark-and-Compact와 다른 점은 조각 모음한 객체들이 들어가는 메모리 공간이다. Active메모리 공간에서 In-Active메모리 공간으로 이동하고, 이동한 곳을 Active로 남은 곳을 In-Active로 만들고 전부 free하는 것이다. 메모리 공간이 더 필요하다는 단점이 있다.
현재 GC가 채택하고 있는 알고리즘이다. Weak generational hypothesis를 기반으로 하는 위에서 설명한 GC 프로세스가 바로 Generational Algorithm이다.
Generational Count
(객체가 지금까지 무사 통과한 가비지 수집 횟수)를 센다.Eden
공간에 생성한다.Survivor
영역 중 하나로 이동된다.Old(or Tenured)
에 보관한다.싱글 스레드로 동작하는 GC이며, Mark-and-Compact Algorithm으로 동작한다.
Serial Garbage Collector에서 GC 스레드가 여러개가 된 것이다.
Minor GC를 멀티 스레드로 수행하면서 Promotion 작업이 여러 스레드에서 동시에 발생하면 Corruption이 발생할 수 있다.
PLAB (Parallel-Local Allocation Buffers)
이전 GC 방식은 Mark-Sweep-Compaction에서 단일 스레드가 Old 영역 전체를 훑는 방식이었다면, Parallel Old GC에서는 멀티 스레드가 Old 영역을 논리적으로 균일하게 나눈 Region(2KB 정도의 chunk) 단위로 GC를 수행한다.
Mark-Summary-Compaction 방식
Concurrent Mark & Sweep의 약자로, Low latency Collection에 해당한다.
Java 14에 Drop 됐다
애플리케이션이 구동중일 때 프로세서의 자원을 공유하여 이용가능해야 한다. CMS GC가 수행될 때에는 자원이 GC를 위해서도 사용되므로 응답이 느려질 순 있지만 응답이 멈추지는 않게 된다.
하지만 이러한 CMS GC는 다른 GC 방식보다 메모리와 CPU를 더 많이 필요로 하며, Compaction 단계를 수행하지 않는다는 단점이 있다. 이 때문에 시스템이 장기적으로 운영되다가 조각난 메모리들이 많아 Compaction 단계가 수행되면 오히려 Stop The World 시간이 길어지는 문제가 발생할 수 있다.
문제가 많은 CMS를 대체하기 위해 만들어진 방법으로, 하드웨어의 발전으로 큰 메모리에서 좋은 성능을 내는 것에 초점을 두어 CMS에 비해 Pause 시간을 개선했다.
물리적 Generation 구분을 없애고 전체 Heap을 1~32MB 단위의 Region들로 재편했다. (약 2048개의 Regions들로 나눌 수 있다.)
각 Region은 상태에 따라 역할이 동적으로 부여된다.
G1 GC의 핵심은 Heap을 동일한 크기의 Region으로 나누고, 가비지가 많은 Region에 대해 우선적으로 GC를 수행하는 것이다.
그리고 G1 GC도 다른 가비지 컬렉션과 마찬가지로 2가지 GC(Minor GC, Major GC)로 나누어 수행된다.
Young GC (STW가 발생)
Old GC
ZGC는 아래의 목표를 충족하기 위해 설계된 확장 가능하고 낮은 지연율을 가진 GC이다.
JVM으로 구동되는 애플리케이션의 경우, GC가 동작할 때 pause로 인해 성능에 큰 영향을 미쳐왔다. ZGC는 Load barrier와 Colored Pointer를 함께 사용해 puase 시간을 줄여 성능이 향상되었다.
객체를 가리키는 변수의 포인터에서 64bit를 활용해서 marking
어플리케이션 쓰레드는 힙 메모리에 있는 객체를 참조할때 Load barrier를 만나게 되는데, 이것은 JIT에 의해 주입된 코드다. 이 코드는 주소의 colored point(즉, GC메타데이터 비트)가 bad color인지 체크하고, 만약 bad color라면 객체를 상황에 따라서 mark/relocate/remapping 한다.
STW
: ZGC의 Root에서 가리키는 객체 Mark 표시STW
: 새롭게 들어온 객체들에 대해 Mark 표시STW
: 모든 Root 참조의 재배치를 진행하고 업데이트알고리즘 별 메모리 그림 링크
Mark and Sweep 그림 링크
[JVM] Garbage Collection Algorithms
JVM Garbage Collection(GC)
[JVM] Garbage Collection
NAVER D2 Java Garbage Collection
오랜만에 Garbage Collection 정리
NAVER D2 Java Reference와 GC
자바 메모리 관리 - 가비지 컬렉션
[Java] Garbage Collection 동작 방식
[Java] 다양한 종류의 Garbage Collection(가비지 컬렉션) 알고리즘 (2/2)
[Java] G1 GC의 동작 과정
ZGC
ZGC에 대해서
JVM과 Garbage Collection - G1GC vs ZGC
bump-the-pointer라는 기술을 써서 중간에 빈 공간이 있더라도 해당 공간에 할당하지 않는다.
그럼 Survivor Space의 단편화를 없애기 위하여 + minor GC까지 해서 mark and copy 알고리즘을 사용하는 것.