가비지 컬렉션

.·2022년 4월 20일
0

가비지 컬렉션

개요

많은 사람들이 대규모 어플리케이션 부터 소규모 어플리케이션까지 다양한 목적으로 JAVA SE를 사용하고 있다. HotSpot 가상 머신은 다양한 요구 조건을 충족시키기 위해 각기 다른 가비지 컬렉션을 제공한다. 이번글에서는 가비지 컬렉션에 대해 알아보고 JDK11을 기준으로 사용가능한 가비지 컬렉션을 비교하고 동작 원리를 알아보겠다.

가비지 컬렉션이란

가비지 컬렉션은 어플리케이션의 동적 메모리 할당 요청을 자동으로 관리한다.

  • 운영체제로 부터 메모리를 할당 받고 반환한다.
  • 어플리케이션의 요청에 따라 해당 메모리를 할당한다.
  • 어플리케이션이 사용하고 있는 메모리를 판별한다.
  • 재사용 할 수 있도록 어플리케이션이 사용하지 않는 메모리를 회수한다.

가비지 컬렉션의 주요 개념과 튜닝

프로그램에 있는 모든 객체들의 참조변수에 의해서 접근할 수 없을 때 해당 객체는 가비지로 판단되며 그들의 메모리는 재사용을 위해 회수된다. 가장 간단한 가비지 컬렉션 방법은 항상 모든 살아있는 객체에 접근해 접근 가능한 객체를 확인하는 것이다. 그 후 접근할 수 없는 객체는 가비지로 판별되어 회수된다. 하지만 해당 방식은 많은 시간이 걸리며 대규모 데이터를 다루는 어플리케이션에서는 적절하지 않다.

Reachability

가비지 컬렉터는 객체가 가비지인지 판별하기 위해 접근 가능한지를 살펴본다. 어떤 객체에 유효한 참조가 있으면 'reachable'로, 없으면 'unreachable'로 구별하고, unreachable 객체를 가비지로 간주해 GC를 수행한다. Reachable 한 객체는 크게 4가지로 구분된다.

  • Method 영역의 전역 변수에 의한 참조
  • Stack 영역의 지역 변수, 파라미터에 의한 참조
  • Native Stack 영역에서 생성된 객체에 의한 참조
  • Heap 영역 내의 다른 객체에 의한 참조

Stop The World

Stop-The-World란, GC을 실행하기 위해 JVM이 애플리케이션 실행을 멈추는 것이다. stop-the-world가 발생하면 GC를 실행하는 쓰레드를 제외한 나머지 쓰레드는 모두 작업을 멈춘다. GC 작업을 완료한 이후에야 중단했던 작업을 다시 시작한다. 어떤 GC 알고리즘을 사용하더라도 Stop-The-World는 발생한다.

Generational Collection

HotSpot에는 다양한 가비지 컬레션 알고리즘이 존재하며 모든 알고리즘은 ‘generational collection’ 이라는 기술을 사용한다. 해당 기술은 가비지 탐색 시간을 줄이기 위해 대부분의 객체는 짧은 시간내에 사라진다는 ‘ weak generational hypothesis’ 이라는 가설을 사용한다

객체의 생애주기를 살펴보면 대부분의 객체들은 메모리를 할당 받은 이후 얼마 되지 않아 회수 된다. 가비지 컬렉션 알고리즘의 최적화는 이런 일찍 죽는 객체들에 집중하는 것이다.

Generation

이러한 시나리오를 최적화 하기 위해 객체는 세대별로 관리된다. 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이라는 과정에 따라 진행된다.

  • 새로 생성한 객체를 Eden 영역에 위치시킨다.
  • Eden 영역에서 GC가 한 번 발생한 후 살아남은 객체는 Survivor 영역 중 하나로 이동된다.
  • Eden 영역 혹은 Survival 영역 중 하나가 가득 차면 Minor Collection을 유발한다.
  • Eden 영역과 Survival 영역을 탐색하여 가비지를 판별하고 살아남은 객체들은 비어있는 Survival 영역으로 이동시킨다.
  • 이 과정을 반복하다가 특정 횟수를 넘기거나 Survival 영역에 공간이 부족할 때 해당 객체들을 Old 영역으로 이동시킨다. 해당 과정을 aging이라 한다.

성능 측정 지표

가비지 컬렉션의 성능 측정은 처리량, 지연 시간 2가지 지표를 사용한다.

  • 처리시간(Throughput) : 가비지 컬렉션에 사용되지 않는 총 시간의 백분율
  • 주기(Latency) : 어플리케이션의 응답 속도 , GC가 자주 발생할수록 Lantency가 떨어진다.

일반적으로, 특정 세대의 크기를 조정하는 것은 처리량과 지연 시간 중 중요한 지표를 선택하는 것이다. Old 영역의 GC는 New 영역의 GC에 비하여 상대적으로 시간이 오래 소요되기 때문에 Old 영역으로 넘어가는 객체의 수를 줄이는 것이 가비지 컬렉션에 사용되는 시간을 줄일 수 있다. 예를 들어 Young 영역의 크기를 줄이면 minor GC가 자주 발생하기 때문에 Throughput은 늘어나지만 , GC이 자주 발생하기 때문에 Latency는 늦어진다. 반대로 Young 영역의 크기를 키우면 minor GC가 적게 발생하기 때문에 Throughput은 줄어들지만 Latency가 빨라진다.

성능에 영향을 미치는 요소

  • Heap 크기 : 전체 Heap 영역의 크기는 Throughput과 반비례한다. - 전체 메모리 크기가 커질 수록 가비지 컬렉션을 수행하는 시간이 많이 든다.
  • Young 영역 비율 : Young 영역이 커질수록 Minor 컬렉션의 횟수는 줄어들고 Major 컬렉션의 횟수는 늘어난다. Major 컬렉션의 시간이 Minor 컬렉션의 시간보다 많이 걸리기 때문에 Young 영역의 비율과 Throughput는 반비례한다.

가비지 컬렉터의 종류

Serial Collector

직렬 수집기는 단일 스레드를 사용하여 모든 가비지 수집 작업을 수행하므로 스레드 간 통신 오버헤드가 없어 상대적으로 효율적이다. 멀티프로세서를 사용할 수 없는 단일 프로세서나 작은 데이터 (100 MB 이하)를 사용하는 어플리케이션에 적절하며 -XX:+UseSerialGC 옵션을 통해 사용 가능하다. Young 영역에 대한 GC는 Generational Collection 알고리즘을 통해 이뤄지며 Old 영역에 대한 가비지 컬렉션은 Mark-Sweep-Compact 알고리즘을 통해 이루어진다.

  • 현재 사용되는 객체를 식별한다.

  • 현재 사용되고 있지 않는 객체 (가비지)를 제거한다.

  • 살아남은 객체들을 한 곳으로 모은다.

Parallel Collector

throughput collector로도 불린다. 가비지 컬렉션 성능 향상을 위해 멀티 프로세스를 사용한다. 병렬 수집기는 멀티 프로세서를 지원하는 하드웨어에서 중, 대규모의 데이터를 사용하는 어플리케이션에 적절하며 -XX:+UseParallelGC 옵션을 통해 사용 가능하다.

Young 영역에서의 가비지 컬렉션은 Serial Collector와 같은 방식으로 이루어지지만 멀티 프로세스의 사용으로 인하여 Stop the worold 시간이 훨씬 짧아졌다. Paralle Collector에서는 Serial Collector와 똑같은 방식으로 진행되지만 멀티 프로세스를 통해 병렬적으로 처리한다는 점이 다르고 Parallel-Old Collector에서는 Old 영역의 GC를 Mark-Sweep-Summary 알고리즘을 사용해서 처리한다.

  • Generation의 각 구역들을 고정된 크기를 지닌 Region으로 나눈다.
  • 그 후 멀티 프로세스를 통해 살아있는 객체를 식별한다.

  • 각 구역의 밀집도를 확인하여 살아남은 객체가 많이 밀집한 곳을 Dense Prefix로 지정한다.
  • Dense Prefix로 지정된 구역에 있는 객체들은 움직이지 않는다. 즉 Compact의 대상이 되지 않는다.

  • Dense Prefix로 지정되지 않은 공간들에서 가비지를 제거하고 살아남은 객체들을 한 곳으로 모은다.
  • Sweep과 Compact 과정은 Region 별로 멀티 프로세스에 의해 실행된다.

  • 채워져야 하는 영역을 식별하고 다른 영역에서 객체를 복사해 가져온다.

CMS Collector

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를 더 많이 사용한다는 단점이 존재한다.

G1 Garbage Collector

가비지 우선 수집기는 병렬 쓰레드와 동시 쓰레드를 모두 사용한다. 동시 스레드를 사용하여 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 영역의 가비지 비율이 특정 임계점을 넘으면 시작된다.

  • Initial Marking은 minor GC와 동시에 진행되며 STW event이다. old generation에 존재하는 객체들에 접근 가능한 survival region을 마킹한다.

  • 동시 스레드를 통해 Heap 영역 전체를 살피며 Old 영역에 있는 존재하는 객체의 가비지 여부를 판별한다. 이 때 Young GC가 발생할 수 있다.
  • Region 별로 liveness (가비지 비율)이 계산된다.

  • 비어있는 region은 제거되고 회수된다. Region liveness를 바탕으로 가비지 컬렉션 시간 소요 시간이 유저가 지정한 pause time을 넘기지 않는 region만 가비지 컬렉션의 대상이 된다. region에 살아있는 객체들이 많으면 해당 객체들을 다른 region으로 옮기는데 시간이 많이 걸리기 때문에 가비지 비율이 특정 퍼센트를 넘겨야지만 가비지 컬렉션의 대상이 된다.

  • Region 1 과 Region2의 살아남은 객체들은 region 4로 복사되었다. Region 3는 복사해야하는 객체(70%)가 너무 많기 때문에 가비지 컬렉션의 대상이 되지 않았다.

  • 가비지로 판별된 객체들을 회수한다. - Stop The World
  • 각 Old Region은 다른 Old Region이 참조하는 객체에 대한 정보를 담은 remembered set을 지니고 있다. 해당 영역을 업데이트 한다. - Stop The World
  • 살아남은 객체들을 다른 Old 영역으로 복사한다 - Stop The World
  • 비어있는 region을 초기화 하고 메모리를 회수한다 - 동시스레드를 사용
  • Minor GC와 동시에 수행된다.

  • 살아남은 객체들은 Dark Blue로 표시된 영역으로 복사되었다.

다른 가비지 수집기와의 비교

  • Parellel Collector는 old 영역을 전체적으로만 가비지 수집을 진행할 수 있다. 반면에 G1 Collector는 region이라는 단위로 영역을 나누어 가비지 수집을 진행하며 가비지가 많은 공간을 먼저 회수하기 때문에 가비지 컬렉션에 걸리는 시간을 줄일 수 있다.
  • CMS Collector와 G1 Colletor은 마킹을 진행할 때 동시 스레드를 사용해 어플리케이션 실행 도중 마킹을 진행한다. 하지만 CMS Collector은 Compaction 과정이 없기 때문에 Full GC 실행 시간이 오래 걸린다.
  • G1은 동시쓰레드로 인해 다른 가비지 수집기보다 많은 오버헤드를 사용한다. 이 때문에 많은 양의 메모리를 지닌 멀티 쓰레드 기반의 기계에 적합하다.
  • 높은 확률로 사용자가 원하는 pause time을 보장하며 높은 처리율을 달성한다.

Z Garbage Collector

Z Garbage Collector는 확장 가능한 저지연 가비지 컬렉터이다. Z Garbage Collector는 어플리케이션을 중단하지 않고 모든 과정을 동시 쓰레드로 진행한다. ZGC는 낮은 대기 시간(10ms 미만의 일시 중지)을 요구하거나 큰 용량을 지닌 힙(테라바이트)을 사용하는 애플리케이션을 위해 설계되었다. JDK 11부터 사용 가능하며 -XX:+UseZGC 옵션을 통해 활성화 할 수 있다.

가비지 컬렉터 선택

  • 100 MB이하의 작은 메모리를 사용하거나 단일 프로세스를 사용하는 어플리케이션은 직렬 수집기를 사용한다. XX:+UseSerialGC 옵션을 통해 사용 가능하다.
  • 어플리케이션의 처리량이 첫번째 우선순위이고, 일시 정지 시간에 대한 요구사항이 없거나 1초 이상의 중지시간을 허용하며, 멀티 스레드를 지원하는 어플리케이션은 병렬 수집기를 사용한다. XX:+UseParallelGC
    옵션을 통해 활성화 할 수 있다.
  • 응답시간이 처리량보다 더 중요하며 응답시간을 1초 이하로 유지하고 싶을 때는 동시 수집기를 사용한다. JDK11 기준으로 CMS Collector는 G1 Collector로 대체되었으며 기본 수집기로 G1 Collector가 설정되어 있기 때문에 따로 설정을 해주지 않으면 G1 Collector를 사용 가능하다.
  • 응답 시간이 가장 큰 우선순위이며 10ms 미만의 응답 시간을 요구하고 테라바이트 이상의 용량을 지닌 힙을 가지고 있을 때는 Z Collector를 사용한다. 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:MaxMetaspaceSizeto 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 기술을 통해 각 스레드마다 버퍼를 할당하여 각 스레드가 자신이 가지고 있는 버퍼에만 접근할 수 있도록 만들어 스레드 세이프한 환경을 만들었다.

정리

  • GC는 운영체제로부터 메모리를 할당 받아, 어플리케이션 요청에 따라 메모리를 할당하고, 현재 사용되지 않는 객체를 판별한 후, 재사용 가능하도록 해당 공간을 회수하는 과정이다.
  • GC는 Heap을 young 영역과 old 영역으로 나누어 각 객체를 세대별로 관리하는 Genration Collection을 공통으로 사용한다.
  • GC의 튜닝은 가비지 컬렉터 종류의 선택, 힙 크기 조절, young, old 영역의 비율 조절을 통해 이루어지며 가비지 컬렉션의 총 소요되는 시간, 가비지 컬렉션의 발생 주기, 응답 시간들을 고려해서 이루어진다.
  • Java 11을 기준으로 사용가능한 가비지 컬렉터는 Serial, Parellel, G1, Z 가비지 컬렉터가 존재한다.
  • Old 영역의 GC 과정은 가비지 컬렉터의 종류에 따라 다르며 상황에 맞게 선택해야한다.

Reference

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

profile
지금부터 공부하고 개발한것들을 꾸준하게 기록하자.

0개의 댓글