뭐라도 끄적이는 BLOG

Garbage Collector의 종류 (Serial, Parallel, CMS, G1, Z) 본문

Java/Java 기본

Garbage Collector의 종류 (Serial, Parallel, CMS, G1, Z)

Drawhale 2023. 6. 29. 06:21

GC의 원리

GC를 수행하기 위해서 몇가지 가설이 필요하다. 대표적으로 Weak Generational Hypothesis가 있다. 이 가설은 대부분의 객체는 빠르게 unreachable한 상태로 전환이 된다고 보고 있다. 특수한 경우를 제외하고 객체들이 중괄호가 끝나는 시점에서 더이상 사용되지 않고 unreachable한 상태가 되어 GC의 대상이 된다.

위 가설은 Oracle사에서 관찰하여 증명이 되어있다. 해당 자료에서 대부분의 객체가 빠르게 소멸하는 것을 알 수 있다.

Mark And Sweep Algorithm

root set으로 부터 출발하여, 참조되는 객체들에 대해서 마크를 하게 된다. 이 단계를 Mark Phase라고 한다. 이후 마크되지 않은 객체들을 추적하여 삭제를 한다. 삭제하는 단계를 Sweep Phase라고 한다.

해당 알고리즘은 메모리가 단편화되는 단점이 있다. 이를 해결하기 위한 알고리즘이 Mark And Compact Algorithm이다.

Mark And Compact Algorithm

Mark And Sweep Algorithm 처럼 참조되는 객체들에 대해서 마크를 하고, 참조되지 않으면 삭제를 한다. 이후 메모리를 정리하여 메모리 단편화를 해결할 수 있도록 한다. 대부분의 GC에서 해당 알고리즘을 사용한다.

Stop The World

GC를 수행하기 위해 JVM이 멈추는 현상을 말한다. GC가 작동하는 동안 GC관련 Thread를 제외한 모든 Thread가 멈추게 된다. 일반적으로 JVM을 공부하는 이유가 Stop The World 시간을 최소화 하기 위한 튜닝을 위해 공부하게 된다.

GC의 종류

  • Serial GC
  • Parallel GC
  • CMS GC
  • G1 GC
  • Z GC

Serial GC

단일 스레드에서 동작하는 GC이다. 이 GC가 실행될때 모든 애플리케이션 스레드를 정지 시킨다. 따라서 다중 스레드 어플리케이션에서 Serial GC를 사용하는것은 좋은방법이 아니며 CPU코어 개수가 적을때 사용하는 방식이다.

mark-sweep-compact 알고리즘을 사용한다. Mark and Sweep과정에서 Comapct라는 과정이 추가된 알고리즘이다. Young영역에서의 GC는 MinorGC를 사용하며 Old 영역의 GC에 해당 알고리즘을 사용한다. Old영역에서 살아 있는 객체를 Mark하여 heap의 앞부분 부터살아 있는 것만 남긴다. 마지막 단계에서 각 객체들이 연속되게 쌓이도록 heap의 가장 앞 부분부터 채워서 객체가 존재하는 부분과 객체가 없는 부분으로 나눈다.

java -XX:+UseSerialGC -jar Application.java

Parallel GC

JVM의 기본 GC이며 Throughput Collectors라고도 한다. 다중 스레드를 이용하며 GC를 수행하는 동안 다른 애플리케이션의 스레드도 정지시킨다. 메모리가 충분하고 코어의 개수가 많을 때 사용하면 좋다. 다음은 Serial GC와 Parallel GC의 스레드를 비교한 그림이다.

이미지 출처: "Java Performance", p. 86

java -XX:+UseParallelGC -jar Application.java

Concurrent Mark Sweep GC (CMS GC)

Stop-The-World가 발생하면 GC를 실행하는 스레드를 제외한 나머지 스레드는 모두 작업을 멈춘다. 그리고 GC작업을 완료한 이후 중단된 작업을 다시 시작한다. Concurrent Mark Sweep GC는 Stop-The-World를 줄이는 GC이다. System.gc()와 같이 명시적으로 GC를 호출하면 Concurrent Mode Failure / Interruption이 발생한다.

https://d2.naver.com/helloworld/1329 (원본은  찾지 못함)

CMS GC의 단계를 살펴보면 아래와 같다.

  1. 초기 Initial Mark단계에서 클래스 로더에서 가장 가까운 객체 중 살아 있는 객체만 찾는다. 여기서 Stop-The-World시간은 매우 짧다.
  2. Concurrent Mark 단계에서 방금 살아 있다고 확인한 객체에서 참조하고 있는 객체들을 따라가면서 확인한다. 이 단계의 특징은 다른 스레드가 실행중인 상태에서 동시에 진행된다는 것이다.
  3. Remark단계에서는 Concurrent Mark단계에서 새로 추가되거나 참조가 끊긴 객체를 확인한다.
  4. Concurrent Sweep단계에서 쓰레기를 정리하는 작업을 실행한다. 이 작업도 다른 스레드가 실행되고 있는 상황에서 진행된다.

이렇게 Stop-The-World시간이 짧아 애플리케이션의 응답 속도가 매우 중요할때 CMS GC를 사용한다. 다른이름으로 Low Latency GC라고도 부른다. 하지만 다른 GC방식보다 메모리와 CPU를 더 많이 사용하고 Compaction단계가 제공되지 않는다는 단점이 있다.

CMS GC를 사용하기 위해서는 아래와 같은 명령어를 사용한다.

java -XX:+UseParNewGC -jar Application.java
Java9부터는 CMS GC는 더이상 사용하지 않으며 Java 14부터는 CMS GC지원을 완전히 중단하였다.
Java9 - JEP 291, Java14 - JEP363 참고
>> java -XX:+UseConcMarkSweepGC --version
Java HotSpot(TM) 64-Bit Server VM warning: Option UseConcMarkSweepGC was deprecated 
in version 9.0 and will likely be removed in a future release.
java version "9.0.1"
>> java -XX:+UseConcMarkSweepGC --version
OpenJDK 64-Bit Server VM warning: Ignoring option UseConcMarkSweepGC; 
support was removed in 14.0
openjdk 14 2020-03-17

G1GC

The Garbage-First Garbage Collector" (TS-5419), JavaOne 2008, p. 19)

이전 Heap에서 YoungGen과 OldGen영역을 명확하게 구분하던 GC들과 다르게 물리적으로 구분하지 않는다. 개념적으로는 존재하지만 Heap Area를 일정 크기의 region으로 구분하여 논리적으로 구분하고 있다. Region은 기본적으로 [전체 힙메모리 / 2048]로 지정되어 있다. 이전 GC들과 마찬가지로 특수한 경우(크기가 너무 큰경우)를 제외하고 최초 객체가 생성이 되면 Eden에 할당하고, 이후 Survivor로의 이동과 소멸, 그리고 Old Region으로 이동하는 생명주기를 가진다.

JDK6에서는 G1 GC를 시험적으로 사용할수 있으며 정식으로 포함한건 JDK7부터이다.

G1GC의 동작방식

G1GC는 Young-only Phase와 Space Reclamation Phase를 반복한다. Young Only Phase는 MinorGC만 수행하다가 XX:InitiatingHeapOccupancyPercent(Old Generation 비율)에 지정된 값을 초과하는 순간 MajorGC가 수행된다. Young Only Phase가 끝나면 Space Reclamation Phase가 시작된다. 해당 Phase에서는 MixedGC가 수행되는데 Mark단계가 없어서 STW빈도가 Young Only Phase에 비해 줄어든다. Space Reclamation Phase가 끝나면 다시 Young Only Phase로 돌아가서 MinorGC를 수행한다.

ZGC

ZGC는 Linux용 실험 옵션으로 Java11에서 출시한 low-latency GC이다. JDK 14에서 부턴 Windows 및 macOS에서도 ZGC를 도입하였고 Java15부터 정식 지원한다.

ZGC의 목표는 GC로 인한 일시 정지 시간이 10ms를 초과하지 않아야하며, 비교적 적은(수백 MB)크기에서 매우 큰(수 TB) 사이즈의 heap을 다룰 수 있어야 한다. G1 GC보다 애플리케이션 처리량이 15%이상 떨어지지 않고 colored pointers, load barriers를 사용하여 미래의 GC를 위한 기능/최적화 기반을 마련한다.

  • ZGC는 확장 가능한 low latency GC
  • ZGC는 모든 비싼 작업들을 concurrently하게 수행하는데, 그러면서도 애플리케이션 스레드들이 약간의 millisecond 이상 멈추지 않도록 한다. 따라서 ZGC는 low latency를 요구하는 애플리케이션에 적합하다.
  • 정지 시간은 사용되고 있는 heap의 크기와 무관하다.
  • ZGC는 8MB부터 16TB까지의 heap 크기를 지원한다.
  • ZGC는 커맨드 라인에서 -XX:+UseZGC옵션을 주면 사용할 수 있다.

JDK11부터 실험적으로 도입되어 Java15 이전에는 ZGC를 활성화하기 위해 다음과 같은 명령어를 사용한다.

java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC Application.java

Java15 이후에는 아래와 같은 명령어를 사용한다.

java -XX:+UseZGC Application.java

ZGC의 단계 (정리가 필요함)

ZGC는 객체를 찾는 Mark라는 단계가 있습니다. GC는 여러 방법으로 객체 상태 정보를 저장할 수 있습니다. 예를 들어 Key가 메모리 주소이고 Value가 해당 주소에 있는 객체의 상태인 Map을 만들 수 있습니다.

ZGC는 참조 상태를 참조 비트로 저장합니다. 이것을 reference coloring이라고 부릅니다. 그러나 이 방법으로 객체에 대한 메타데이터를 저장하기 위해 참조 비트를 설정한다는 것은 상태 비트가 객체의 위치에 대한 정보를 보유하지 않기 때문에 여러 참조가 동일한 객체를 가리킬 수 있습니다.

그리고 메모리 단편화를 줄이기위해 ZGC는 relocation(재배치)를 사용합니다. heap이 크만 relocation은 느려지게 됩니다. ZGC는 긴 일시 중지 시간을 원하지 않기 때문에 대부분의 relocation을 애플리케이션과 병렬로 수행합니다. 하지만 이건 새로운 문제를 만들게 됩니다.

객체에 대한 참조가 있다고 가정해 보겠습니다. ZGC는 이를 relocation하고 context switch가 발생합니다. 여기서 애플리케이션은 스레드가 실행되고 이전 주소를 통해 이 객체에 엑세스하려고 합니다. ZGC는 이를 해결하기 위해 load barrier를 사용합니다. load barrier는 스레드가 힙에서 참조를 가져오기 전에 참조에 대해 일부 처리를 수행할 수 있습니다. 따라서 완전히 다른 참조를 생성할 수 있습니다. 이것을 remapping이라고 부릅니다.

1. Marking

ZGC의 마킹은 3단계로 나눌 수 있다.

  1. stop-the-world: root 참조를 찾아 표시한다. root 참조는 힙의 객체에 도달하기 위한 시작점이다.
  2. concurrent: root참조에서 시작하여 객체 그래프를 탐색한다. 그리고 도달하는 모든 객체에 mark를 한다. 또한 load barrier가 표시되지 않은 참조를 감지하면 그 참조도 mark한다.
  3. 마지막 단계로 약한 참조와 같은 일부 케이스를 처리하기 위한 stop-the-world단계를 진행

이 시점에서 객체에 도달할 수 있는 대상을 알수있게 됩니다. ZGC는 이러한 표시를 위해 mark0와 mark1 메타데이터 비트를 사용합니다.

2. Reference Coloring

참조는 가상 메모리에서 바이트의 위치를 나타냅니다. 그러나 이를 위해 참조의 모든 비트를 사용할 필요는 없습니다. 일부 비트는 참조의 속성을 나타낼 수 있습니다. 이것을 reference coloring이라고 부릅니다.

32bits 시스템에선 4GB까지 사용할 수 있습니다. 하지만 이젠 64bits컴퓨터가 보급화 되어 더 많은 메모리를 사용할 수 있습니다. 그리고 32bits에서는 coloring에 사용할 수 있는 bit가 없습니다. 그렇기 때문에 ZGC는 64bits 참조를 사용하게 되고 이것은 ZGC가 64bits 플랫폼에서만 사용할 수 있다는 것을 나타냅니다.

https://www.baeldung.com/jvm-zgc-garbage-collector

ZGC참조는 42bits를 사용하여 주소를 나타냅니다. 이는 ZGC참조에선 4TB까지의 메모리 공간을 처리할 수 있다는 것을 의미합니다. 그 이후 4bits는 참조 상태를 저장할 수 있습니다. 이러한 bits를 메타데이터 비트라고 합니다. ZGC에서 이러한 메타데이터 중 한비트는 반드시 1이 됩니다.

  • finalizable bit - finalizer를 통해서만 객체에 도달 할 수 있습니다.
  • remap bit - 참조가 최신 상태이며 객체의 현재 위치를 가리킵니다. (relocation)
  • marked0, marked1 bits - 도달 가능한 객체를 표시하는 데 사용됩니다.

3. Relocation

ZGC에서 Relocation은 다음 단계로 구성됩니다.

  1. 블록을 찾는 concurrent단계에서 블록을 재배치하고 relocation set에 넣습니다.
  2. stop-the-world단계는 relocation set의 모든 root 참조를 재배치하고 해당 참조를 업데이트합니다.
  3. concurrent단계는 relocation set에 남아 있는 모든 객체를 재배치하고 이전 주소와 새 주소 간의 매핑을 forwarding table에 저장합니다.
  4. 나머지 참조의 재작성은 다음 marking단계에서 발생합니다. 이렇게 하면 객체 트리를 두번 탐색할 필요가 없습니다. load barriers도 이를 수행할 수 있습니다.

4. Remapping and Load Barriers

relocation단계에서 재배치된 주소에 대한 대부분의 참조는 다시 작성하지 않았습니다. 따라서 이러한 참조를 사용하면 원하는 객체에 액세스 할 수 없습니다. 더군다나 garbage에 접근할 수도 있습니다.

ZGC는 이 문제를 해결하기 위해 Load Barriers를 사용합니다. Load Barriers는 remapping이라는 기술을 사용하여 재배치된 객체를 가리키는 참조를 수정합니다.

애플리케이션이 참조를 로드할 때 load barrier을 트리거한 다음 올바른 참조를 반환하기 위해 다음 단계를 따릅니다.

  1. remap bit가 1로 설정되어 있는지 확인합니다. 1로 설정되어 있다면 참조가 최신 상태임을 의미하여 안전하게 반환할 수 있습니다.
  2. 참조된 객체가 relocation set에 있는지 여부를 확인합니다. 그렇지 않은 경우 재배치하고 싶지 않다는 의미합니다. 다음에 이 참조를 로드할 때 이 검사를 피하기 위해 remap bit를 1로 설정하고 업데이트된 참조를 반환합니다.
  3. 이제 액세스하려는 객체가 재배치 대상이 아니라는 것을 알 수 있습니다. 이제 이전이 발생했는지에 대한 여부를 생각해봅니다. 객체가 재배치된 경우 이단계를 건너뛰고 다음 단계를 수행합니다. 그렇지 않으면 재배치하고 재배치된 각 객체에 대한 새 주소를 저장하는 전달 테이블에 항목을 만듭니다. 이후 다음 단계를 수행합니다.
  4. 이제 객체가 재배치되었음을 확인할 수 있습니다. 참조를 객체의 새 위치로 업데이트하고 (이전 단계의 주소를 사용하거나 forwarding table에서 검색하여) remap bit를 설정한후 참조를 반환합니다.

위 단계를 통해 객체에 액세스하려고 할 때마다 객체에 대한 가장 최근 참조를 얻을 수 있습니다. 참조를 로드할 때마다 load barrier에 트리거 되며 특히 재배치된 객체에 처음 액세스할때 애플리케이션 성능이 저하되지만 이건 짧은 일시 중지 시간을 원한다면 지불해야 하는 대가 입니다. 그리고 이런 단계들은 상대적으로 빠르기 때문에 애플리케이션 성능에 큰 영향을 미치진 않습니다.


※ 참고 자료

https://www.blog-dreamus.com/post/zgc%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C

https://steady-coding.tistory.com/590

 

 

 

반응형

'Java > Java 기본' 카테고리의 다른 글

Java 연산자  (0) 2023.07.01
Java Variables와 Data Types  (0) 2023.06.30
JVM 구성 요소  (0) 2023.06.29
Java Bytecode  (0) 2023.06.29
Java (JVM, JIT, JDK 간략한 설명)  (0) 2023.06.29