많은 사람들이 대규모 어플리케이션 부터 소규모 어플리케이션까지 다양한 목적으로 JAVA SE를 사용하고 있다. HotSpot 가상 머신은 다양한 요구 조건을 충족시키기 위해 각기 다른 가비지 컬렉션을 제공한다. 이번글에서는 가비지 컬렉션에 대해 알아보고 JDK11을 기준으로 사용가능한 가비지 컬렉션을 비교하고 동작 원리를 알아보겠다.
가비지 컬렉션은 어플리케이션의 동적 메모리 할당 요청을 자동으로 관리한다.
프로그램에 있는 모든 객체들의 참조변수에 의해서 접근할 수 없을 때 해당 객체는 가비지로 판단되며 그들의 메모리는 재사용을 위해 회수된다. 가장 간단한 가비지 컬렉션 방법은 항상 모든 살아있는 객체에 접근해 접근 가능한 객체를 확인하는 것이다. 그 후 접근할 수 없는 객체는 가비지로 판별되어 회수된다. 하지만 해당 방식은 많은 시간이 걸리며 대규모 데이터를 다루는 어플리케이션에서는 적절하지 않다.
가비지 컬렉터는 객체가 가비지인지 판별하기 위해 접근 가능한지를 살펴본다. 어떤 객체에 유효한 참조가 있으면 'reachable'로, 없으면 'unreachable'로 구별하고, unreachable 객체를 가비지로 간주해 GC를 수행한다. Reachable 한 객체는 크게 4가지로 구분된다.
Stop-The-World란, GC을 실행하기 위해 JVM이 애플리케이션 실행을 멈추는 것이다. stop-the-world가 발생하면 GC를 실행하는 쓰레드를 제외한 나머지 쓰레드는 모두 작업을 멈춘다. GC 작업을 완료한 이후에야 중단했던 작업을 다시 시작한다. 어떤 GC 알고리즘을 사용하더라도 Stop-The-World는 발생한다.
HotSpot에는 다양한 가비지 컬레션 알고리즘이 존재하며 모든 알고리즘은 ‘generational collection’ 이라는 기술을 사용한다. 해당 기술은 가비지 탐색 시간을 줄이기 위해 대부분의 객체는 짧은 시간내에 사라진다는 ‘ weak generational hypothesis’ 이라는 가설을 사용한다
객체의 생애주기를 살펴보면 대부분의 객체들은 메모리를 할당 받은 이후 얼마 되지 않아 회수 된다. 가비지 컬렉션 알고리즘의 최적화는 이런 일찍 죽는 객체들에 집중하는 것이다.
이러한 시나리오를 최적화 하기 위해 객체는 세대별로 관리된다. GC는 각 세대의 공간이 꽉 차면 발생한다. 객체들이 할당되는 Heap 영역은 크게 Young 영역과 Old 영역으로 나뉜다. Young 영역에서 발생하는 GC를 Minor Collection이라 부르고 Old 영역에서 발생하는 GC를 Major Collection이라 부른다. 대부분의 객체는 Young 영역에 할당되며 그곳에서 메모리가 회수 된다
Minor Collection
Young 영역은 Eden 영역과 Survival 영역으로 나뉘며 Survival 영역은 다시 2가지로 나뉜다. Survival 0, Survival 1 라는 이름은 해당 영역들의 구분을 위해 임의로 붙힌 이름이다. Minor Collection은 aging이라는 과정에 따라 진행된다.
가비지 컬렉션의 성능 측정은 처리량, 지연 시간 2가지 지표를 사용한다.
일반적으로, 특정 세대의 크기를 조정하는 것은 처리량과 지연 시간 중 중요한 지표를 선택하는 것이다. Old 영역의 GC는 New 영역의 GC에 비하여 상대적으로 시간이 오래 소요되기 때문에 Old 영역으로 넘어가는 객체의 수를 줄이는 것이 가비지 컬렉션에 사용되는 시간을 줄일 수 있다. 예를 들어 Young 영역의 크기를 줄이면 minor GC가 자주 발생하기 때문에 Throughput은 늘어나지만 , GC이 자주 발생하기 때문에 Latency는 늦어진다. 반대로 Young 영역의 크기를 키우면 minor GC가 적게 발생하기 때문에 Throughput은 줄어들지만 Latency가 빨라진다.
직렬 수집기는 단일 스레드를 사용하여 모든 가비지 수집 작업을 수행하므로 스레드 간 통신 오버헤드가 없어 상대적으로 효율적이다. 멀티프로세서를 사용할 수 없는 단일 프로세서나 작은 데이터 (100 MB 이하)를 사용하는 어플리케이션에 적절하며 -XX:+UseSerialGC
옵션을 통해 사용 가능하다. Young 영역에 대한 GC는 Generational Collection 알고리즘을 통해 이뤄지며 Old 영역에 대한 가비지 컬렉션은 Mark-Sweep-Compact 알고리즘을 통해 이루어진다.
throughput collector로도 불린다. 가비지 컬렉션 성능 향상을 위해 멀티 프로세스를 사용한다. 병렬 수집기는 멀티 프로세서를 지원하는 하드웨어에서 중, 대규모의 데이터를 사용하는 어플리케이션에 적절하며 -XX:+UseParallelGC
옵션을 통해 사용 가능하다.
Young 영역에서의 가비지 컬렉션은 Serial Collector와 같은 방식으로 이루어지지만 멀티 프로세스의 사용으로 인하여 Stop the worold 시간이 훨씬 짧아졌다. Paralle Collector에서는 Serial Collector와 똑같은 방식으로 진행되지만 멀티 프로세스를 통해 병렬적으로 처리한다는 점이 다르고 Parallel-Old Collector에서는 Old 영역의 GC를 Mark-Sweep-Summary 알고리즘을 사용해서 처리한다.
Concurrent Mark Sweep Collector은 가비지 컬렉션의 STW 시간을 줄이고 어플리케이션이 실행될 때 동시에 GC를 진행하는 가비지 컬렉터이다. CMS Collector은 JAVA 11 기준으로 G1 Collector로 대체되었으며 현재는 사용하지 않는다.
Initial Mark 단계에서는 클래스 로더에서 가장 가까운 객체까지만 Reachability를 판별한다. 이 때 STW 가 일어나며 그 후 어플리케이션을 실행하며서 동시에 Concurrent Mark를 진행한다. 즉 Initial Mark에서 살아있다고 판별한 객체들을 따라가면서 추가적으로 접근 가능한 객체가 있는지 살펴본다. 그 후 Remark 단계에서 Concurrent Mark 단계에서 새로 추가되거나 참조가 끊긴 객체를 확인하는데 이 때도 STW가 발생한다. 마지막으로 어플리케이션을 실행하면서 동시에 Concurrent Sweep 단계에서 가비지로 판별된 인스턴스를 제거한다.
CMS Collector은 STW가 짧은 대신 기본적으로 Compaction 단계를 제공하지 않고 메모리와 CPU를 더 많이 사용한다는 단점이 존재한다.
가비지 우선 수집기는 병렬 쓰레드와 동시 쓰레드를 모두 사용한다. 동시 스레드를 사용하여 JAVA 프로그램이 실행되는 동안 살아있는 객체를 검색하며 병렬 스레드를 통해 객체를 빠르게 복사하고 일시 중지 시간을 낮게 유지한다. 가비지 우선 수집기는 JAVA 9부터 default 수집기로 설정되어 있다.
G1은 heap를 region이라는 작은 단위로 나눈다. region은 Genrational Collection에서 설명했던 young genration region, old generation region이 될 수 있다. Young generation 메모리는 연속되지 않는 region으로 구성되며 이는 메모리공간을 재조정하는 것을 효율적으로 만든다. Minor GC는 Stop-The-World를 일으키며 다중 스레드를 사용해 병렬방식으로 처리된다. Major GC는 Initial Mark - Concurrent Marking - Remark - Cleanup 과정을 통해 이루어지며 Old 영역의 가비지 비율이 특정 임계점을 넘으면 시작된다.
다른 가비지 수집기와의 비교
Z Garbage Collector는 확장 가능한 저지연 가비지 컬렉터이다. Z Garbage Collector는 어플리케이션을 중단하지 않고 모든 과정을 동시 쓰레드로 진행한다. ZGC는 낮은 대기 시간(10ms 미만의 일시 중지)을 요구하거나 큰 용량을 지닌 힙(테라바이트)을 사용하는 애플리케이션을 위해 설계되었다. JDK 11부터 사용 가능하며 -XX:+UseZGC
옵션을 통해 활성화 할 수 있다.
XX:+UseSerialGC
옵션을 통해 사용 가능하다.XX:+UseParallelGC
XX:+UseZGC
옵션을 통해 활성화 할 수 있다.Major GC는 Old 영역에서만 가비지를 수집하는 것일까, 모든 영역에서 가비지를 수집할까 ?
Memory Management in the Java HotSpot™ Virtual Machine 은 Java 5를 기준으로 작성되었다. 해당 문서를 살펴보면 다음과 같은 문단이 존재한다. Serial Collector와 Parellel Collector의 Major GC의 전체 과정을 살펴볼 수 있다.
When the old or permanent generation fills up, what is known as a full collection (sometimes referred to as a major collection) is typically done. That is, all generations are collected. Commonly, the young generation is collected first, using the collection algorithm designed specifically for that generation
Old 또는 Permanent 영역이 가득 차면 Full GC(Major GC)가 발생한다. 모든 영역들이 탐색된다. 가장 먼저 genration 알고리즘을 통해 Young Generation에서 가비지가 수집된다.
Then what is referred to below as the old generation collection algorithm for a given collector is run on both the old and permanent generations. If compaction occurs, each generation is compacted separately.
그 후 Old 영역과 Permanent 영역에서 가비지가 수집된다. 그리고 압축은 각 공간에서 독립적으로 실행된다.
**HotSpot Virtual Machine Garbage Collection Tuning Guide 은 Java 11을 기준으로 작성되었다. 해당 문서를 살펴보면 9장 Garbage-First Collector에서 다음과 같은 문단이 존재한다. 해당 문단에서는 G1 Collector의 Major GC 과정을 살펴볼 수 있다.
As backup, if the application runs out of memory while gathering liveness information, G1 performs an in-place stop-the-world full heap compaction (Full GC) like other collectors.
liveness를 계산하는 Concurrent Marking phase에서 young 영역에서 메모리가 부족해지면 Full GC를 실행한다. 즉 G1 가비지 수집기에서는 Major GC가 항상 Minor GC를 유발하지는 않으며 Major GC를 진행하다가 Young 영역에 메모리가 부족해지면 Minor GC를 실행한다.
Permanant Generation은 무엇인가?
**HotSpot Virtual Machine Garbage Collection Tuning Guide 은 Java 11을 기준으로 작성되었다. 해당 문서를 살펴보면 12장 Other Consierations에서 다음과 같은 문단을 살펴볼 수 있다.
Java classes have an internal representation within Java Hotspot VM and are referred to as class metadata. In previous releases of Java Hotspot VM, the class metadata was allocated in the so-called permanent generation. Starting with JDK 8, the permanent generation was removed and the class metadata is allocated in native memory. The amount of native memory that can be used for class metadata is by default unlimited. Use the option
-XX:MaxMetaspaceSize
to put an upper limit on the amount of native memory used for class metadata.
Java 8부터 Permanent 영역은 제거되고 class metadata 영역이 생겼다. 해당 영역은 클래스 정보들을 저장하는 공간으로 JVM의 메모리 구조에서 살펴본 Method 영역에 해당한다. 해당 영역은 기본적으로 용량의 제한이 없지만 옵션을 통해 메모리 제한을 걸어줄 수 있다.
Java Hotspot VM explicitly manages the space used for metadata. Space is requested from the OS and then divided into chunks. A class loader allocates space for metadata from its chunks (a chunk is bound to a specific class loader). When classes are unloaded for a class loader, its chunks are recycled for reuse or returned to the OS.
Hotspot VM은 OS로 부터 메모리를 할당 받아 chunk로 나눈다. 클래스 로더는 chunk에 클래스 정보를 할당하며 클래스가 로드되지 않았을 때 chunk는 회수되어 OS로 반납된다. 위에서 봤듯이 Class Metadata 영역은 Heap영역이 아니지만 Major GC의 대상이 된다.
Minor GC가 일어날 때 Old 영역에 있는 객체가 참조하는 객체는 어떻게 판별할까 ?
JAVA Magazine **Understanding Garbage Collectors 을 살펴보면 다음과 같은 문단이 나온다.
The old objects need to be updated to reference the new locations for the new objects. The JVM does this by maintaining a summarization data structure called the card table. Whenever a reference is written into an old-generation object, the card table is marked so that during the next young GC cycle, the JVM can scan this card looking for old-to-young references.
가비지 컬렉터는 카드 테이블이라는 데이터 구조를 통해 young 영역에서만 가비지 수집을 진행할 수 있다. Old 영역에 존재하는 객체가 새로운 객체를 참조할 때 카드 테이블안에 그 정보가 쓰여진다. 이 때문에 minor GC를 실행할 때 Old 영역 전체를 탐색할 필요 없이 Card Table만 검색을 진행하면 된다.
G1 must maintain a card table data structure so that it can collect only young regions. It also must maintain a record for each old region that other old regions have references to. This data structure is called an into remembered set.
추가적으로 G1은 다른 Old 영역에 대한 참조 정보는 remembered set이라는 자료구조를 통해 저장한다. 즉 모든 old region에는 다른 young 영역에 대한 참조 정보를 저장하는 card table, 다른 old 영역에 대한 참조 정보를 저장하는 remebered set을 통해 각 지역에 대한 탐색 시간을 줄인다.
빠른 메모리 할당을 위한 기술에는 무엇이 존재할까 ?
Memory Management in the Java HotSpot™ Virtual Machine 은 Java 5를 기준으로 작성되었다. 해당 문서를 살펴보면 다음과 같은 문단을 살펴볼 수 있다.
bump-the-pointer technique. That is, the end of the previously allocated object is always kept track of. When a new allocation request needs to be satisfied, all that needs to be done is to check whether the object will fit in the remaining part of the generation and, if so, to update the pointer and initialize the object.
bump-the-pointer 기술은 마지막에 할당된 객체를 추적한다. 새로운 객체의 생성 요청이 들어오면 영역의 남은 공간에 남은 공간이 있는지 확인하고 pointer를 업데이트 한 후 생성하면 된다. 따라서 새로운 객체를 생성할 때 마지막에 추가된 객체만 점검하므로 메모리 할당이 빠르게 이루어진다.
For multithreaded applications, allocation operations need to be multithread-safe. If global locks were used to ensure this, then allocation into a generation would become a bottleneck and degrade performance. Instead, the HotSpot JVM has adopted a technique called Thread-Local Allocation Buffers (TLABs). This improves multithreaded allocation throughput by giving each thread its own buffer
멀티 스레드 환경에서는 스레드 세이프를 위해 락이 필요하며 이는 성능 저하를 일으킨다. 대신 TLAB 기술을 통해 각 스레드마다 버퍼를 할당하여 각 스레드가 자신이 가지고 있는 버퍼에만 접근할 수 있도록 만들어 스레드 세이프한 환경을 만들었다.
Memory Management in the Java HotSpot™ Virtual Machine - JAVA 5
Getting Started with the G1 Garbage Collector - JAVA 7
HotSpot Virtual Machine Garbage Collection Tuning Guide - JAVA 11
Understanding Garbage Collectors - JAVA 12
Java Garbage Collection -Naver D2
Java Reference와 GC - Naver D2
Garbage Collection 튜닝 - Naver D2