JVM에서 Heap 영역에 남아있는, 더 이상 사용되지 않는 인스턴스들을 가비지라고 하며, 메모리가 부족해질 경우 JVM은 이 가비지들을 삭제하여 추가로 사용할 수 있는 메모리 공간을 만든다. 이 과정을 가비지 콜렉팅 이라고 하며, 이를 수행하는 것을 가비지 콜렉터, 또는 GC 라고 한다.
GC는 Weak generational hypothesis라는 두 가지 가설에 의해서 설계되었다.
1번의 경우, 통상적으로 어떤 메서드를 작성한다고 가정한다고 했을 때, 그 과정에서 선언되어 할당된 객체는 메서드가 종료되면, 더 이상 사용되지 않아 필요가 없어진다(리턴을 하거나 매개변수에 대입하는 행위를 하지 않는다고 가정).
2번의 경우 1번의 케이스와 비슷하게, 금방 접근 불가능 상태가 되어 이후 새로 생성되는 젊은 객체를 참조하는 경우는 거의 없을 것이라고 생각한다. 물론 Spring 같이 Framework 레벨의 객체는 예외.
Heap내에서 객체의 수명을 관리하기 위해 Young, Old 구역으로 나뉜다.
Young 영역은 다시 Eden, 두 개의 Survivor 영역으로 나뉜다.
Eden에는 처음 생성된 객체가 위치하게 된다. 그러다 Eden 영역이 꽉 차면 Minor GC 가 발생하게 되면, 이 영역에 위치하는 객체 중 참조되지 않는 객체는 메모리에서 제거되며, 살아남은 객체들은 Survivor 영역 두 군데 중 한 군데로 이동하게 된다. Minor GC 가 발동할 때마다, Survivor 영역에 있던 객체들은 다른 Survivor 영역으로 이동한다. 즉 최초에 Survivor 1 영역에 있던 객체는, Minor GC가 발동하면 Survivor 2 영역으로 이동하게 된다.
각 객체는 Minor GC에서 살아남은 횟수를 기록하는 age bit 를 가지고 있으며, 이 age bit는 Minor GC가 발생할 때마다 하나씩 증가한다. age bit값이 MaxTenuringThreshold 라는 설정값을 초과하게 되는 경우, Old Generation 영역으로 객체가 이동하게 된다. (JVM 옵션 : -XX:MaxTenuringThreshold 통해 설정할 수 있다. 기본값은 JVM에 의해 동적으로 정해진다.)
추가) Permanent 구역은 Heap이 아니며, Java 8 이후로는 Metaspace 영역으로 대체되었다.
GC 알고리즘의 기본 흐름은 GC 대상을 식별하고, 식별된 대상을 메모리에 제거하며, 필요한 경우 최적화까지 수행한다. 간단해 보이지만 이러한 알고리즘에도 여러 종류가 있으며, 각각 다음과 같다.
Garbage의 탐색에 초점을 맞춘 초기 알고리즘이다. 각 객체마다 Reference Count 라는 것을 관리하는데, 말 그대로 참조 되고 있는 갯수를 의미하며, Reference Count가 0이되면 GC를 수행한다. 단순한 구조인데다가, Reference Count가 0이 되면 즉시 메모리에서 해제된다는 장점이 있으나, Reference Count를 계속 관리해주어야 하고, Linked List 같은 순환 참조 구조에서 Memory Leak이 발생할 가능성이 크다.
기본적인 GC 과정으로, 다양한 GC에서 사용되는 알고리즘이다.
이름 그대로, GC 대상 객체를 식별(Mark)하고, 청소(Sweep)하고, 압축(Compaction, 앞에서부터 채움)한다.
Root Set에서 시작하는 Reference의 관계를 추적하며, Tracing Algorithm이라고도 불린다.
Mark 단계에서는 Garbage 대상 외 살아남을 객체를 마킹하며, 마킹 방식은 각 객체의 Header에 Flag를 심거나 별도의 BitmapTable을 이용한다.
Sweep 단계는 Mark 단계가 끝나면 바로 실행되며, 마킹이 없는 객체를 모두 삭제하는 작업을 한다. Sweep이 완료되면 살아남은 모든 마킹 정보를 초기화한다.
Compact 단계에서는 Sweep 단계 후 살아남은 객체들 사이사이의 빈 공간, 즉 단편화를 살아남은 객체들을 이어붙여 해결한다. 이 작업 이후 살아남은 객체들의 Reference를 업데이트하는 작업이 필요하여 부가적인 Overhead가 수반된다.
JVM 에는 생각보다 많은 종류의 GC 알고리즘이 있다. 참고로, Java 7, 8은 기본 GC로 Parallel GC를 사용하고, Java 9, 10 은 G1 GC를 사용한다고 한다. Java 11부터는 실험적 기능인 Z GC를 사용할 수 있다. 15부터는 정식 기능으로 출시할 예정인 듯 하다.
순차적인 GC 라는 의미로, Mark-Sweep-Compaction 알고리즘이 한 번에 하나씩만 동작한다. 가장 오래된 GC이며, 요즘에는 사용되지 않고, 사용해서도 안된다. Stop-The-World 시간이 너무 길기 때문.
Serial GC가 하나의 스레드로 Mark-Sweep-Compaction을 수행한다면, Parallel GC는 여러 개의 스레드로 Mark-Sweep-Compaction을 수행한다. 이로 인해 Stop-The-World 시간이 줄어들게 된다.
Parallel GC와 비슷하나, Mark-Sweep-Compaction 알고리즘 대신 Mark-Summary-Compaction 알고리즘을 사용한다. Summary 작업은 Sweep 작업에 살아있는 객체를 식별하는 작업이 추가된 것이다. (이름만 봐서는 이게 더 옛날 GC같기도..)
앞의 GC 방식보다 더 개선되었으면서, 복잡한 방식이다. Stop-The-World 시간을 최소화 하는데 초첨을 맞췄다. 컨셉은 GC 대상 객체를 최대한 자세히 파악한 후, Stop-The-World 가 발생하는 Sweep 시간을 최소화 하는데 초점을 맞췄다. Low Latency GC라고도 부른다.
CMS GC는 총 4단계에 걸쳐 이루어진다.
Initial Mark
GC 과정에서 살아남을 객체를 Root Set에서 가장 가까운 객체만 탐색하며, 참조가 끊겼는지를 확인한다. 이 과정에서 Stop-The-World 가 일어나지만, 탐색 깊이가 짧아 멈추는 시간 역시 짧다.
ConcurrentMark
Initial Mark 단계에서 GC 대상으로 판별한 객체들을 따라가며 GC 대상인지 추가로 확인한다. 이 과정중에는 Stop-The-World 가 일어나지 않는다.
Remark
Concurrent Mark 단계의 결과를 검증한다. Concurrent Mark 단계에서 GC 대상으로 추가 확인되었는지, 참조가 제거되었는지 등 확인을 한다. 이때 Stop-The-World가 일어나며, 이 시간을 최대한 줄이기 위해 멀티스레드로 검증작업을 수행한다.
Concurrent Sweep
GC 대상으로 판별된 객체들을 멀티스레드로 메모리에서 제거한다. 이때 Stop-The-World가 발생하지 않는다.
단점으로는 하는 일이 많다보니 CPU 부하가 커진다는 것이고, Compaction이 기본적으로 제공되지 않고 필요할 때만 일어나는데, 이때의 Stop-The-World 시간이 다른 GC보다 더 길게 걸릴 수도 있다.
하드웨어가 발전되어 JVM을 가동하는 메모리의 크기도 점점 커져가는데, 이전까지의 GC들은 큰 용량의 메모리에 적합하지 않다(Root set부터 순차적으로 탐색하기에 용량이 클 수록 탐색 시간이 길어짐).
G1 GC는 이런 점을 개선하여, 큰 Heap 메모리에서 짧은 GC 시간을 보장하는데 그 목적을 둔다.
G1 GC는 앞서 살펴본 Eden, Survivor, Old 영역이 존재하지만, 고정된 크기로 고정된 위치에 존재하지 않는다. 전체 Heap 영역을 Region이라는 특정한 크기로 나눠서, 각 Region의 상태에 따라 역할(Eden, Survivor, Old)이 동적으로 부여된다. 2048개의 Region으로 나뉠수 있으며, 옵션을 통해 1MB~32MB 사이로 지정할 수 있다.
앞에서 봤던 Eden, Survivor, Old 외에 Humonogous와 Available/Unused Region이 추가로 보인다.
Humongous는 설정된 Region 크기의 50%를 초과하는 큰 객체를 저장하기 위한 공간으로, 이 Region에서는 GC 동작이 최적으로 동작하지 않는다.
Available/Unused 는 이름에서 짐작할 수 있듯, 아직 사용되지 않은 공간이다.
Young 영역에서 GC가 수행되면 Stop-The-World 현상이 발생하며, 이 시간을 줄이기 위해 멀티 스레드로 GC를 수행한다. 동작 방식은 기존이랑 비슷한데, Eden, Survivor 영역에서 살아남은 다른 Survivor 영역으로 이동되며, 비워진 Region은 Available/Unused 상태로 돌린다.
Full GC가 수행되면 총 6개의 단계를 거쳐 이루어지게 된다.
Initial Mark
Old Region에 존재하는 객체들이 참조하는 Survivor Region을 찾는다. 이 과정에서 Stop-The-World가 발생하게 된다.
Root Region Scan
Initial Mark 에서 찾은 Survivor Region에서 GC 대상 객체를 탐색한다.
Concurrent Mark
전체 Region에 대해 스캔하여, GC 대상 객체가 존재하지 않는 Region은 이후 단계에서 제외된다.
Remark
Stop-The-World 후, GC 대상에서 제외할 객체를 식별한다.
Cleanup
Stop-The-World 후, 살아있는 객체가 가장 적은 Region에 대해서 참조되지 않는 객체를 제거한다. Stop-The-World 끝내고 완전히 비워진 Region을 Freelist에 추가하여 재사용한다.
Copy
Root Region Scan 단계에서 찾은 GC 대상 Region이었지만 Cleanup 단계에서 살아남은 객체들을 Available/Unused Region에 복사하여 Compaction 작업을 수행한다.
(개인적인 궁금증: 어짜피 Concurrent Mark에서 GC 대상 객체에 대해 탐색하는데, Initial Mark와 Root Region Scan 단계가 필요한 이유?)
비교적 최근에 나온 GC이며, 아래의 목표를 충족하기 위해 설계된 확장 가능하고 낮은 지연율(low latency)을 가진 GC이다.
이대로라면 지금까지 나온 어떤 GC보다 혁신적에 가까운데, 어떻게 이게 가능할까?
ZGC는 ZPages라는 G1 GC의 Region과 비슷한 영역의 개념을 사용하지만, Region은 고정된 크기인 것에 반해 ZPages는 크기가 2MB의 배수로 동적으로 생성 및 삭제될 수 있다.
사이즈별 Heap 영역
중요 포인트
자세한 작동원리는 한글 문서로는 못 찾아서.. 원문 찾아서 다시 정리해야 할 듯
이후 그림도 직접 그려서 추가할 예정
https://velog.io/@litien/가비지-컬렉터GC
https://d2.naver.com/helloworld/1329
https://d2.naver.com/helloworld/329631
https://d2.naver.com/helloworld/37111
https://mirinae312.github.io/develop/2018/06/04/jvm_gc.html
https://www.holaxprogramming.com/2013/07/20/java-jvm-gc/
https://medium.com/@joongwon/jvm-garbage-collection-algorithms-3869b7b0aa6f
https://sarc.io/index.php/java/2098-zgc-z-garbage-collectors
http://cr.openjdk.java.net/~pliden/slides/ZGC-PLMeetup-2019.pdf
https://initproc.tistory.com/entry/G1-Garbage-Collection
이해하기 어렵지만.. 평상시 궁금했던 부분 인데 감사합니다ㅠㅠ😂