GC

Myeon·2020년 11월 17일
0

GC (Garbage Collecton)란? 더이상 사용하지 않는 객체 등을 메모리에서 해제(삭제)하는 JVM의 작업
Java 프로세스가 동작하는 과정에서 GC는 불필요한 또는 더이상은 사용하지 않는 객체들을 메모리에서 제거함으로써, Java 프로세스가 한정된 메모리를 효율적으로 사용할 수 있게 해준다.

GC에 대해서 공부하기 전, 꼭 알고 있어야 할 게 한가지가 있다.

Stop-The-World

Stop-The-World는 직역하면 세상을 멈춘다는 거대한 말같지만 어플리케이션에 정말 거대한 영향을 미치는 아이다. 쉽게 말해 가비지컬렉터를 실행하기 위해 JVM이 애플리케이션 실행을 멈추는 것이다. Stop-The-World이 발생하면 gc를 실행하는 쓰레드를 제외한 나머지 쓰레드는 모두 멈춘다. 그래서 이 시간이 길면 길수록 어플리케이션이 느려진다. 그리고 어떤 gc 알고리즘을 사용하던지, Stop-The-World는 발생하기에 이 시간을 줄이는 것이 gc알고리즘의 키포인트다.

그리고 아래를 보기전에 Mark-And-Sweep이 뭔지도 알면 쉽기에 정리.

사용되지 않는 객체를 식별하는 작업 (Mark)

사용되지 않는 객체를 제거하는 작업 (Sweep)

파편화된 메모리 영역을 앞에서부터 채워나가는 작업 (Compaction)

그리고, GC는 weak generational hypothesis 아래 내용에 의해서 도입되었다.

① 대부분의 객체는 금방 unreachable이 된다.

    : 아래 for문 안에서 생성된 100개의 Obj는 더이상 사용되지 않을 것.

for (int i = 0; i < 1000; i++){ 
	NewObject obj = new NewObject(); 
	obj.doSomething(); 
}

② 오래된 객체에서 생성된지 얼마안된 객체로의 참조는 적다. - POJO객체(Plain Old Java Object)

    : 보통 어떤 값이나 상태를 저장하기 위해 POJO 객체를 생성하고, 다른 메소드나 클래스에 전달하고, 다 사용한 객체는 더이상 사용하지 않는다.

NewObject obj = new NewObject("value"); doSomething(obj);

그럼 GC에 대해서 살펴보자!

JVM의 Heap은 이런 구조로 영역이 나누어져 있다.


그리고 위의 Heap구조에서 다음과 같이 GC가 일어난다.

Object가 맨처음 생성되었을때, 그것은 Eden공간 내부에서 생성되고, 그 후 Minor GC후 살아남은 객체는 S1으로 이동, S1이 꽉 차면 Minor GC 이후 S2으로 이동된다. 이후 S2도 꽉 차게 되면 minor GC이후 다시 S1으로 이동한다. 그리고 이동하면서 살아남은 횟수를 기록하는 age bit가 +1씩 올라간다.

이런 식으로 S1, S2영역을 번갈아가며 살아남은 객체들은 Old 영역으로 옮겨진다.(이 과정을 Promotion이라고 한다.) 이 영역에서는 Major GC가 일어나며, 이 영역이 꽉 차면 Full GC가 일어난다.

Young -> Old 영역으로 이동하는 기준은 Servivor영역이 꽉 차거나, Max age bit가 되었을 때 이다. (MaxTenuringThreshold 값이며, JVM 옵션 : -XX:MaxTenuringThreshold 로 설정할 수 있다.)

  • minor GC : Young 영역에서 발생하는 GC

  • major GC : Old 영역이나 Perm 영역에서 발생하는 GC / (FULL GC)

GC 종류

  1. Serial GC

Mark-Sweep-Compaction 알고리즘을 사용하여, 순차적으로 동작한다. 쓰레드가 하나로 작용한다. CPU코어가 하나 일때 사용

  • Mark-Sweep-Compaction Algorithm

이 알고리즘의 첫 단계는 Old 영역에 살아 있는 객체를 식별(Mark)하는 것이다. 그 다음에는 힙(heap)의 앞 부분부터 확인하여 살아 있는 것만 남긴다(Sweep). 마지막 단계에서는 각 객체들이 연속되게 쌓이도록 힙의 가장 앞 부분부터 채워서 객체가 존재하는 부분과 객체가 없는 부분으로 나눈다(Compaction).

  1. Parallel GC

SerialGC 와 동작은 같지만, Minor GC만 멀티 스레드로 동작한다. (-XX:ParallelGCThreads=N 로 스레드 수를 조절)

  1. Parallel Old GC

Parallel Old GC는 JDK 5 update 6부터 제공한 GC 방식이다. Mark-Summary-Compaction 알고리즘 사용한다.

  1. CMS GC

stw 시간을 최소화 하는데에 초점을 맞춘 GC. GC 대상을 최대한 자세히 파악한 후, 정리하는 시간(stw)을 짧게 가져가겠다는 컨셉이다.

다만 GC 대상을 파악하는 과정이 복잡한 여러단계로 수행되기 때문에 다른 GC 대비 CPU 사용량이 높고, Compaction이 되지 않기 때문에 데이터 파편화로 인해 메모리부족 현상이 있을 수 있다. 따라서 fullGC시간이 굉장히 길다.

1) Initial Mark : Root Set에 의해 직접 참조되는 객체들을 선택 (Root Set: 현재 참조중인 객체를 판단하기 위한 메모리 주소 모음) - STW

2) Concurrent Mark : 애플리케이션 동작 중 살아있는 객체 식별

3) Remark: 2번에서 새로 추가로 참조가 끊긴 객체를 확인 - STW

4) Concurrent Sweep : 참조가 끊긴 모든 객체를 정리

  1. G1 GC (Garbage First) (java7~)

G1 GC는 앞서 살펴본 GC와는 다른 방식으로 힙 메모리를 관리한다. 앞서 살펴보았던 Eden, Survivor, Old 영역이 존재하지만 고정된 크기로 고정된 위치에 존재하는 것이아니며, 전체 힙 메모리 영역을 Region 이라는 특정한 크기(2048개의 regions)로 나눠서 각 Region의 상태에 따라 그 Region에 역할(Eden, Survivor, Old)이 동적으로 부여되는 상태이다.


Humongous : Region 크기의 50%를 초과하는 큰 객체를 저장하기 위한 공간이며, 이 Region 에서는 GC 동작이 최적으로 동작하지 않는다. (있으면 설계를 다시 살펴봐야한다 함..)


Initial Mark : Old Region 에 존재하는 객체들이 참조하는 Survivor Region 을 찾는다. - STW

Root Region Scan : Initial Mark 에서 찾은 Survivor Region에 대한 GC 대상 객체 스캔 작업을 진행한다.

Concurrent Mark : 전체 힙의 Region에 대해 스캔 작업을 진행하며, GC 대상 객체가 발견되지 않은 Region 은 이후 단계를 처리하는데 제외되도록 한다.

Remark : 최종적으로 GC 대상에서 제외될 객체(살아남을 객체)를 식별해낸다. - STW

Cleanup : 살아있는 객체가 가장 적은 Region 에 대한 미사용 객체 제거 수행한다. 이후 STW를 끝내고, 앞선 GC 과정에서 완전히 비워진 Region 을 Freelist에 추가하여 재사용될 수 있게 한다. - STW

Copy : GC 대상 Region이었지만 Cleanup 과정에서 완전히 비워지지 않은 Region의 살아남은 객체들을 새로운(Available/Unused) Region 에 복사하여 Compaction 작업을 수행한다.

java10부터는 Parallel Full GC for G1기능이 반영되어 Full GC를 병렬로 만들어, G1의 worst-case latency가 개선되었다.

java12부터는 아래 기능이 반영되었다.

1) Abortable Mixed Collections for G1

G1이 효율적으로 동작하도록 하기 위해 중단 가능한 Collection을 가지도록 변경 (가비지 컬렉션을 수행하기 위해 수행 대상 regions = Collection Set)

STW가 정해진 시간 내에 불필요한 객체들을 수집하지 못할 경우, GC를 중단할 수 있음.

collection set을 mandatory part, optional part 둘로 나누고, mandatory part는 young regions와 같이 G1 GC가 점진적 처리를 할 수 없는 영역으로 구성하지만, 효율성을 위해 old regions도 포함.

그리고 나머지는 old regions로만 구성된 optional part가 된다. 이 과정의 결과로 collection set은, 예를 들어 80%의 mandatory part, 20%의 optional part와 같이 나뉘게 된다.

우선적으로 필수적인 부분에서 수집을 진행하고, 이후 남은 시간에 선택적 부분에서 수집을 진행하되, 남아있는 정지시간에 선택적 부분을 수집하지 못하리라 판단하면, 이를 다음 GC 시간의 선택적 부분으로 넘기게 된다.

2) Promptly Return Unused Committed Memory from G1

GC가 활성화된 상태일때 Java의 힙 메모리 일부를 운영체제에 반환하여 JVM 힙 크기를 조정한다.

원래 G1은 Full GC가 일어나거나 Concurrent cycle이라는 상황에만 Java의 힙 메모리를 운영체제에 반환

보통은 Concurrent cycle만 해당 반환작업을 일으킬 수 있는데, 외부에서 강제하지 않는 한 대부분의 경우에는 힙 메모리를 반환하지 않음. 이 문제에 대한 개선

  1. ZGC (java 11~)

ZGC는 대기 시간이 낮은 확장 가능한(scalable low latency) GC이다. ZGC는 모든 종류의 비싼 작업을 동시에(concurrently) 작업하며, 애플리케이션 스레드의 실행을 중지하지 않는다는 특징이 있다.

ZGC는 10ms 미만의 짧은 대기 시간이 필요하거나 테라 바이트 큐모의 매우 큰 heap을 사용하는 애플리케이션을 위한 GC이다. ZGC의 주요 원리는 Load barrier와 Colored object pointer를 함께 사용. 이를 통해 Java의 애플리케이션 스레드가 동작하는 중간에, ZGC가 객체 재배치 같은 작업을 수행할 수 있게 해준다.

  1. Shenandoah : A Low-Pause-Time Garbage Collector (Experimental) (java12~)

실행 중인 Java 쓰레드와 동시에 GC를 실행하여 GC 중지 시간을 단축하는 알고리즘으로. 힙 크기와는 무관하게 동일한 중지 시간을 유지

이 알고리즘은 처리량과 메모리 공간의 효율성보다는 응답성에 초점을 둔 GC다. (비교적 최근에 나온 GC라 문서가 많이 없다....)

마지막으로, JVM GC Options

-XX:NewRatio

Young 영역과 Old 영역의 비율을 조정한다. NewRatio = Old 영역의 크기 / Young 영역의 크기

NewRatio의 값이 너무 작으면 Out Of Memory가 발생하기 쉽고, 너무 크면 Old 영역으로 옮겨지는 객체의 수가 많아진다.

보통 서버 애플리케이션은 Young : Old == 1 : 2 정도로 잡고, 클라이언트 애플리케이션은 1 : 3~5 정도로 잡는다고 한다.

오라클 JVM의 기본값은 2이다.

-XX:SurvivorRatio

Eden 영역과 Survivor 영역의 비율을 조절한다. SurvivorRatio = Eden 영역의 크기 / (Survivor 영역의 크기 / 2)

Survivor 영역이 너무 작아지면 객체의 age가 threshold에 도달하기 전에 Old 영역으로 넘어가 버리며, Survivor 영역이 너무 크면, Minor GC가 너무 자주 발생하게 된다.

오라클 JVM의 기본값은 8이다.

-XX:MaxTenuringThreshold

Young 영역에 있는 객체가 얼마만큼의 Minor GC가 발생한 후 Old 영역으로 옮겨질 것인지에 대한 threshold를 설정한다.

오라클 JVM의 기본값은 15이다.

https://d2.naver.com/helloworld/1329
https://mirinae312.github.io/develop/2018/06/04/jvm_gc.html

0개의 댓글