Java - Garbage Collection(GC,가비지 컬렉션) 란?

2019. 7. 29. 22:41프로그래밍언어/Java&Servlet

 

 

Java - JVM이란? JVM 메모리 구조

초심으로 돌아갈 때가 된 것 같다. 오늘 포스팅할 내용은 JVM(Java Virtual Machine)이다. 사실 지금까지는 스킬 베이스의 공부만 해왔었다. 하지만 점점 개발을 하다보니 성능이라는 것에 굉장히 큰 관심이 생겼..

coding-start.tistory.com

이전 포스팅에서는 초심으로 돌아가 JVM에 대해 포스팅을 하였습니다. 이번 포스팅은 JVM의 Heap 영역에서 이루어지는 Garbage Collection에 다루어 보려고 합니다. 본 포스팅에서 Hotspot JVM의 GC를 다룰 것입니다.

 

Garbage Collection은 무엇일까? GC란 이미 할당된 메모리에서 더 이상 사용하지 않는 메모리를 해제하는 행동을 의미한다. 사용되지 않는 메모리의 대상은 Heap과 Method Area에서 사용되지 않는 Object를 의미한다. 그리고 소스상의 close()는 Object 사용중지 의사표현일 뿐 Object를 메모리에서 삭제하겠다는 의미가 아니다. 물론 개발자는 System.GC()를 명시적으로 호출할 수 있지만 주의해야 할 것이 있다면 해당 메소드 호출은 Full GC를 수행시키는 메소드이기 때문에 Stop-the-world 시간이 길고 무거운 작업이며 또한 반드시 즉시 수행한다는 보장도 없는 메소드이기에 사용을 지향하는 메소드이다.

 

GC에 대해 자세히 다루어보기 전에 과연 GC로 인한 문제점이 무엇이 있을까?

 

GC라는 자동화 메커니즘으로 인해 프로그래머는 직접 메모리를 핸들링 할 필요가 없게 되었다. 더불어 잘못된 메모리 접근으로 인한 Crash 현상의 소지도 없어지게 되었으니 더 없이 편리한 기능이 아닐 수 없다. 그러나 GC는 명시적인 메모리 해제보다 느리며 GC 순간 발생하는 Suspend Time(Stop-the-world) 시간으로 인해 다양한 문제를 야기시킨다.(애플리케이션 동작이 멈춰 클라이언트는 한순간 애플리케이션이 멈춰보인다.)

 

Root Set과 Garbage

Garbage Collection은 말 그대로 Garbage를 모으는 작업인데 여기서 Garbage란 사용되지 않는 Object, 즉 더 이상 참조되지 않는 Object를 뜻한다. 좀 더 자세히 설명하면 Object의 사용 여부는 Root Set과의 관계로 판단하게 되는데 Root Set에서 어떤 식으로든 참조 관계가 있다면 Reachable Object라고 하며 이를 현재 사용하고 있는 Object로 간주하며 그 밖의 Object는 Unreachable Object가 된다.

 

물론 위 그림에는 없지만 참조되지만 더 이상 사용되지 않는 Object인 Memory Leak Object도 존재한다. 뒤에서 정확히 다루어볼 것이다.

 

Root Set은 더 구체적으로 아래와 같이 3가지 참조 형태를 통해 Reachable Object를 판별한다.

  1. Local variable Section, Operand Stack에 Object의 참조 정보가 있다면 Reachable Object이다.
  2. Method Area에 로딩된 클래스 중 constant pool에 있는 참조 정보를 토대로 Thread에서 직접 참조하지 않지만 constant pool을 통해 간접으로 참조되고 있는 Object는 Reachable Object이다.
  3. 아직 메모리에 남아 있으며 Native Method Area로 넘겨진 Object의 참조가 JNI 형태로 참조 관계가 있는 Object는 Reachable Object이다.

위의 3가지 경우를 제외하면 모두 GC 대상이 된다.

 

위에서 설명한 Memory Leak 객체의 구체적인 사례를 살펴 볼 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Main{
    public static void main(String args[]){
        Leak lk = new Leak();
        for(int i=0;i<9000000;i++){
            lk.addList(a);
            lk.removeStr(a);
        }
    }
}
 
class Leak{
    ArrayList lst = new ArrayList();
 
    public void addList(int a){
        lst.add("abcdefg");
    }
 
    public void removeStr(int i){
        Object obj = lst.get(i);
        obj=nulll;
    }
}
cs

위의 코드는 어떻게 될까? 아마 OutOfMemoryException이 발생할 것이다. removeStr()을 통해 참조를 null로 바꾸고 있는 객체는 List에 들어있는 객체의 참조이지만 문자열 객체 그 자체가 아니기에 List 안에 있는 문자열 객체는 그대로 남아 있을 것이다. 하지만 사용자 입장에서는 삭제했다고 생각하고 더 이상 List에 들어있는 문자열을 사용하지 않을 것이다. 이것을 바로 Memory Leak한 Object라고 한다. 다른 표현으로는 Reachable but not Live라고 표현한다.

 

GC가 수행된 메모리의 해지는 할당된 그 자리에서 이루어지며 그로 인해 Heap 메모리 단편화 문제가 발생한다.(객체를 할당할 자리는 충분하지만 자리가 듬성듬성 나있고 연속적이지 않아 객체를 할당하지 못하는 현상) JVM에서는 이러한 단편화를 해소하기 위해 compaction 같은 알고리즘을 사용한다. 하지만 compaction은 비용이 큰 작업이기에 TLAB(Thread Local Allocation Block)을 사용하여 단편화 문제를 해결하기도 한다. 이것은 뒤에서 더 자세히 다룰 것이다. 여기까지 GC는 Root Set에서 참조되지 않는 메모리 자원을 반납하는 행위. 즉, 한정된 자원인 Heap이라는 메모리 공간을 재활용하려는 목적인 것이다. 

 

Hotspot JVM의 Garbage Collection

Hotspot JVM은 기본적으로 Generational Collection 방식을 사용한다. 즉, Heap을 Object Generation 별로 Young Area와 Old Area로 구분하며 Young Area는 다시 Eden Area와 Survivor Area 두개로 구분하여 사용된다.

 

GC 메커니즘은 경험적 지식(Weak Generational Hypothesis)으로 두 가지 가설을 두고 있는데 첫째로 대부분의 객체는 생성된 후 금방 Garbage가 된다. 둘째로 Old 객체가 Young 객체를 참조할 일은 드물다라는 것이다.

 

첫번째 가설을 보면 새로 할당되는 객체가 모인 곳은 메모리 단편화가 발생할 확률이 높다고 간주한다. 메모리 할당은 기존 객체 다음 주소에 할당하게되며 Garbage는 먼저 할당된 부분에서 많이 생길 것이다. Sweep(Mark되지 않은 객체 제거)을 하게되면 메모리 단편화가 발생하게 되며 이후 Compaction처럼 비용이 높은 작업을 해야한다. 이 때문에 객체 할당만을 위한 전용공간인 Eden Area를 만들게 된 것이고 Minor GC 당시 Live한 객체를 옮기는 Survivor Area를 따로 구성한 것이다.

 

Garbage를 추적하는 부분은 Tracing 알고리즘을 사용한다. 위쪽 그림에서도 설명한 RootSet에서 참조 관계를 추적하고 참조되고 있는 객체는 Marking 처리한다. 이러한 Marking 작업도 Young 영역에 국한 되는데 Marking 작업은 메모리 Suspend 상황에서 수행되기 때문이다. 만약 Old 영역까지 Marking 작업을 수행한다면 그만큼 Suspend(Stop-the-world)시간이 길어질 것이다. 하지만 만약 Old 영역에서 Young 영역의 객체를 참조하고 있다면 어떻게 해야할까? 분명 Marking 처리를 하지 않는다고 했는데? 이런 경우를 처리하기 위하여 Card Table이라는 것이 존재한다.

 

Card Table

Card Table이란 Old 영역의 메모리를 대표하는 별도의 메모리구조이다.(실제로 Old 영역의 일부는 Card Table로 구성됨) 만약 Young 영역의 객체를 참조하는 Old 영역의 객체가 있다면 해당 Old 영역의 객체의 시작주소에 카드를 Dirty로 표시하고 해당 내용을 Card Table에 기록한다.

 

 

이후 해당 참조가 해제되면 표시한 Dirty Card도 사라지게끔 하여 참조 관계를 쉽게 파악할 수 있게 한다. 이러한 Card Table 덕분에

Minor GC 과정에서 Card Table의 Dirty Card만 검색하면 빠르게 Old 영역에서 참조되는 Young 영역의 객체를 알 수 있다. 참고로 Card Table은 Old 영역 512byte당 1byte의 공간을 차지한다고 한다.

 

TLAB(Thread-Local Allocation Buffers)

 

Garbage Collection이 발생하거나 객체가 각 영역에서 다른 영역으로 이동할 때 애플리케이션의 병목이 발생하면서 성능에 영향을 주게 된다. 즉, T1이 할당하기 전까지 T2~T5는 락이 걸려 대기하게 될 것이다. 그래서 Hotspot JVM에서는 스레드 로컬 할당 버퍼(TLAB)라는 것을 사용한다. 이를 통해 각 스레드별 메모리 버퍼를 사용하면 다른 스레드에 영향을 주지 않는 메모리 할당 작업이 가능하게 된다. 하지만 TLAB도 최초 할당 때와 할당된 TLAB이 부족하여 새로이 할당을 받을 때에는 동기화 이슈가 발생한다. 하지만 객체 Allocation 작업 횟수에 비하면 동기화 이슈가 대폭 줄기 때문에 Allocation에 걸리는 시간은 상당히 줄어든다.

 

여기서 하나 더 그림의 특징이 있다. 스레드가 객체를 할당할때 메모리 주소의 맨 뒷 공간에 할당하는 것이 보인다. 이것은 Bump-the-pointer라는 기술로써 어딘가에 주소 맨 뒷 자리 정보를 캐싱해 놓고 매번 마지막 주소에 할당하는 방법인 것이다.

 

GC 대상 및 범위

GC 대상 Area는 Young, Old Generation과 Java 1.7 기준 Permanent Area인데 Hotspot JVM은 각 Generation 별로 각각 GC를 수행한다.(Java 1.8에서도 Static 변수, 상수 등은 Heap 메모리에 할당되므로 GC 대상이 된다.) Young Generation에서 발생하는 GC를 Minor GC라고 한다. Young Generation은 빈번하게 GC가 수행되는 영역이며 성숙된 Object는 Old Generation으로 Promotion된다. 여기서 성숙된 Object라 함은 특정 횟수 이상 참조되어 기준 Age를 초과한 Object를 말한다. Promotion 과정 중 Old Generation의 메모리도 충분하지 않은 경우면 GC를 수행하는 데, 이는 Full GC(Major GC)라고 한다. 해당 GC는 Young 영역보다 비교적 큰 메모리를 GC하는 과정이므로 Suspend(Stop the world) 시간이 긴 GC 과정이다. Perm 영역에서도 메모리가 부족할 수 있는 데, 이는 너무 많은 수의 클래스가 로딩되어 여유 공간이 없어진 경우이다. 이 경우에도 Young+Old에 여유공간이 많다고 해도 Full GC가 발생한다.

 

GC 관련 옵션

 

옵션 상세 설명
-Client Client Hotspot VM으로 구동
-Server Server Hotspot VM으로 구동

-Xms<Size>

-Xmx<Size>

Yound Generation의 최소,최대 크기를 설정
-Xss<Size> Stack 사이즈 설정
-XX:MaxNewSize=<Size>

Young Generation의 최대 크기 설정

1.4 버전 이후 NewRatio에 따라 자동 계산

<Java 1.7>

-XX:PermSize

-XX:MaxPermSize

Perm 영역의 초기 크기와 최대 크기(Default 64MB)를 설정

<Java 1.8>

-XX:MetaspaceSize

-XX:MaxMetaspaceSize

Metaspace 영역의 초기 크기와 최대 크기(Default OS 최대 자원 사용) 설정
-XX:SurvivorRatio=<value> 값이 n 이면 n:1:1(Eden:Survivor1:Survivor2=n:1:1) default 8
-XX:NewRatio=<value>

값이 n 이면 Young:Old 비율이 1:n

Client VM은 기본값이 8이며, Server VM은 2이다. 

Intel 계열 CPU를 사용하면 기본값은 12이다.

-XX:TargetSurvivorRatio=<value>

Minor GC는 Eden 영역 뿐만 아니라 Survivor 영역이 Full나도 발생한다. 해당 설정은 Survivor가 Minor GC를 유발하는 비율을 말하며 default=50이다.

즉, Survivor 영역이 50%만 차도 Minor GC가 발생한다.

높게 지정한다면 Survivor 영역 활용도가 높아져 Minor GC 횟수를 조금 줄일 수 있다.

-XX:MinHeapFreeRatio=<percent> 전체 Heap 대비 Free space가 지정된 수치 이하면 Heap을 -Xmx로 지정된 메모리까지 확장한다.(default=40)
-XX:MaxHeapFreeRatio=<percent> 전체 Heap 대비 Free space가 지정된 수치 이상이면 Heap을 -Xms까지 축소한다.(default=70)
-XX:MaxTenuringThreshold=<value> Value만큼 Survivor 영역을 이동하면 그 다음 Minor GC 시간때 Old 영역으로 Promotion한다.
-XX:+DisableExplicitGC System.gc() 함수를 통한 수동 Full GC를 방지한다.

 

Garbage Collector 종류

업무 요건이 복잡해지고 시스템이 점점 거대해지면서 최근에는 자연스럽게 JVM Heap Size 설정이 커지게 되었다. 그러나 그로 인해 GC로 인한 Suspend 현상도 이슈로 대두되게 되었다. Hotspot JVM은 기본적으로 Generation 별로 다른 Garbage Collection 알고리즘을 선택할 수 있다. 아래는 Java 6까지의 Hotspot JVM의 Garbage Collector를 정리한 표이다.

 

Garbage Collector Option

Young Generation 

Collector 알고리즘

Old Generation

Collector 알고리즘

Serial Collector -XX:+UseSerialGC Serial Serial Mark-Sweep-Compact
Parallel Collector -XX:+UseParallelGC Parallel Scavenge Serial Mark-Sweep-Compact
Parallel Compacting Collector -XX:+UseParallelOldGC Parallel Scavenge Parallel Mark-Sweep-Compact
CMS Collector -XX:+UseConcMarkSweepGC Parallel Concurrent Mark-Sweep
G1 Collector -XX:+UseG1GC

Young GC(Evacuation Pause)

Concurrent Mark - Marking

Current Mark - Remarking

Old Region Reclaim - Remarking

Old Region Reclaim - Evacuation Pause

Compaction Phase

 

  • Serial Collector : Client VM의 기본 collector로 한 개의 스레드가 serial로 수행한다.
  • Parallel Collector : 모든 자원을 투입하여 Garbage Collection을 빨리 끝내는 전략으로 대용량 Heap에 적합하다.
  • CMS Collector : Suspend Time을 분산시켜 체감 Pause Time을 줄이고자 할 때 사용한다.
  • G1 Collector : Generation을 물리적으로 구분하지 않고 Region이라는 단위로 쪼개서 관리를 한다. G1 Collector는 이론적으로 거의 Real Time에 가깝게 Suspend Time을 감소시킬 수 있다고 한다.

다양한 GC 알고리즘이 있다. 하지만 여기서 중요한 것은 뭐가 좋다 나쁘다 할 수 없다는 것이다. 애플리케이션의 성격 그리고 현 시스템이 어떠한 시스템이냐에 따라 각 알고리즘의 성능이 달라지기 때문이다. 많은 테스트를 통해 나의 애플리케이션이 어떠한 GC 알고리즘에 더 잘 맞을 지를 선택하는 것이 중요하다. 각 GC 알고리즘에 대해 살펴보자.

 

Serial Collector

Young / Old Generation 모두 Serial로 Single CPU를 사용한다. 즉 1 개의 스레드를 가지고 GC를 수행한다. Client VM의 기본 컬렉터이며 현재는 거의 사용되지 않는 컬렉터이다.

 

Parallel Collector

이 방식은 throughput collector로도 알려진 방식이다. 이 방식의 목표는 다른 CPU가 대기 상태로 남아 있는 것을 최소화 하자는 것이다. Serial Collector와 달리 Young 영역에서의 컬렉션을 병렬로 처리한다. Old 영역은 Serial Collector와 동일한 방식이다. 이는 많은 CPU 자원을 사용하는 반면에 GC의 부하를 줄이고 애플리케이션의 처리량을 증가시킬 수 있다.

 

이는 다수의 스레드가 동시에 GC를 수행하며 적용범위는 Young Area에 국한된다. Old Area는 Serial Mark-Sweep-Compact 알고리즘이 사용된다. 이 컬렉터는 Server VM의 JVM에서 기본 Collector이며 Client VM에서도 옵션 설정으로 사용이 가능하다. 단 CPU가 1개라면 해당 옵션은 무시된다.

 

Mark-Sweep-Compact : 우선 참조관계에 있는 Object를 마킹처리한다.(Mark) 그 이후 마킹처리되지 않은 Object를 자원 해제한다.(Sweep) 자원 해제 후 Heap 메모리의 단편화 해결을 위하여 Compact 작업을 통해 사용되고 있는 메모리와 사용되지 않는 메모리로 구분하여 몰아준다.

 

 

Eden, Survivor Area의 Live Object Copy 작업을 다수의 스레드가 동시에 수행한다.(Suspend 현상 발생) 하지만 투입한 자원만큼 Suspend 시간을 단축할 수 있다. 하지만 여기서 생각해야할 문제가 있다. 다수의 스레드가 Minor GC를 수행하다가 두 개이상의 스레드가 Old Generation 영역에 Promotion 작업을 하게 될 때가 있다. 시간의 간격이 있다면 괜찮지만 만약 동시에 Promotion을 한다면? Corruption이 발생할 수 있다. 하지만 Hotspot JVM은 이러한 문제를 해결하기 위하여 PLAB(Parallel Allocation Buffer)라는 Promotion Buffer를 두어 이러한 문제를 해결하고 있다.

 

 

PLAB(Parallel Allocation Buffer)

위의 그림을 보면 Old Generation으로 Promotion 작업을 두 개의 스레드가 동시에 수행하여 충돌이 나는 상황이 발생하였다. 이러한 문제를 해결하기 위하여 Minor GC를 수행하는 스레드마다 Old Generation의 일정부분을 PLAB이라는 버퍼를 할당하여 충돌상황을 피하고 있다. 하지만 이것도 문제가 아예 없는 것은 아니다. PLAB 때문에 어쩔 수 없이 Old Generation에 메모리 단편화가 생기곤 한다.

 

Parallel Collector 옵션

Parallel Collector의 기본 동작 방식은 애플리케이션 수행시간 대비 GC 수행시간은 default 1%이며, 애플리케이션 수행시간의 최대 90%를 넘지 못한다. 또한 Young Area는 확장할 때 20%씩 증가하고 5%씩 감소한다. GC를 수행하게 되면 최대 Heap Size 대비 5%의 여유 공간을 확보해야 한다.

 

옵션 상세 설명
-XX:+UseParallelGC Parallel Collector 사용 옵션이다.
-XX:ParallelGCThreads=개수 Parallel GC Thread 개수를 설정한다.(기본 값은 CPU 개수)
-XX:+AlwaysTenure

설정하면 Eden Aread의 Reachable Object는 바로 Old Generation으로 Promotion 된다. Parallel Collector 사용시에만 유효하다.

-XX:MaxTenuringThreadshold=0과 같은 설정이다.

-XX:+NeverTenure Survivor Area가 꽉 차는 경우를 제외하고 Object를 Old 영역으로 Promotion 안되게 설정하는 옵션이다. Parallel Collector에서만 유효한 설정이다.(age 자체를 없애서 승격이 안되게 한다.)
-XX:MaxGCPauseMillis=값 설정값 이하로 GC의 Pause Time을 유지하라는 설정이다.
-XX:GCTimeRatio=값 애플리케이션 수행시간 대비 전체 GC의 시간비율을 말한다. 19로 설정하면 1/(1+19)=5% default는 99(1%의 GC Time Ratio)
-XX:+UseAdaptiveSizePolicy GC 회수, Generation의 각 Area 비율, free Heap Size 등 통계 정보를 바탕으로 Young, Old Generation 크기를 자동으로 설정하는 옵션이다.
-XX:YoungGenerationSizeIncrement=값 Young Generation의 확장 비율 설정 옵션(기본값은 20,20%씩 증가)
-XX:AdaptiveSizeDecrementScaleFactor=값

Heap의 Growing Percent 대비 Shrink를 수행할 Ratio 의미 값이 4이고 Heap growing percent가 20 이면 20/4=5%가 된다.

기본값은 4이므로 20/4=5%가 된다.

-XX:GCHeapFreeLimit=값 GC로 확보해야 하는 최소 Free Space를 설정한다. 값은 최대 Heap Size에 대한 비율을 의미한다. Default는 5(%) Out Of Memory Exception 방지를 위한 설정이다.
-XX:MaxHeapFreeRatio=<percent> 전체 Heap 대비 Free Space가 지정 수치 이상이면 -Xms까지 축소하는 옵션이다(기본값 70)

 

CMS Collector

이 방식은 low-latency-collector로도 알려져 있으며, 힙 메모리 영역의 크기가 클 때 적합하다. CMS Collector는 Suspend Time을 분산하여 응답시간을 개선한다. 비교적 자원이 여유 있는 상태에서 GC의 Pause Time을 줄이는 목적이며 Size가 큰 Long Live Object가 있는 경우 가장 적합하다. Young Area에는 Parallel Copy 알고리즘, Old Area에는 Concurrent Mark-Sweep 알고리즘이 사용된다. Young 영역의 GC 알고리즘은 크게 다른 방식과 차이가 없다. 크게 차이가 나는 부분은 Old 영역의 알고리즘이다.

 

Old Area의 Concurrent Mark-Sweep 알고리즘

 

 

위의 그림을 보면 Serial Mark-Sweep-Compact Collector의 알고리즘과 비교하여 Suspend 시간을 분산하고 있는 것이 보인다. 

 

  • Initial Mark Phase 단계 : Single Thread만 사용한다. Suspend 시간이 있으며 애플리케이션에서 직접 참조되는 Live Object만 구별한다. 여기서 직접 참조되는 관계란 RootSet에서 한 단계의 참조관계를 말한다. 깊이가 있는 탐색이 아니라 Suspend 시간이 짧다.
  • Concurrent Mark Phase 단계 : Single Thread만 사용한다. 애플리케이션의 Working Thread와 GC Thread가 같이 수행된다. 즉, 애플리케이션 수행이 가능하다. Initial Mark phase에서 선별된 Live Object가 참조하고 있는 모든 Object를 추적해 Live 여부를 구별한다.
  • Remark Phase 단계 : Multi Thread가 사용되며 애플리케이션이 중지된다. 이미 Marking 된 Object를 다시 추적, Live 여부 확정한다. 이 단계에서는 모든 자원을 투입한다.(많은 자원을 투자한만큼 Suspend 시간이 적다.)
  • Concurrent Sweep Phase 단계 : Single Thread만 사용한다. 애플리케이션은 수행되고 최종 Live로 판명된 Object를 제외한 Dead Object를 지운다. 단 Sweep 작업만 하고 Compaction 작업은 수행 안 한다. 항상 Compaction 작업은 Heap의 Suspend를 전제로 하는데 반복된 Sweep은 단편화를 유발한다. 때문에 Free List를 사용하여 단편화를 줄이는 노력을 한다.

 

CMS Collector를 수행하면 아래와 같은 메모리 단편화 문제가 발생한다. 해당 문제를 해결하기 위해 Free List를 이용한다.

Free List?

Promotion 할당을 할 때 Young Area에서 승격된 Object와 크기가 비슷한 Old Area의 Free space를 Free List에서 탐색하게 된다. 또한 Promotion되는 Object Size를 계속 통계화하여 Free Memory 블록들을 붙이거나 쪼개서 적절한 Size의 Free Memory Chunk에 Object를 할당한다. 그러나 이러한 작업은 GC 수행 중 Young 영역에 부담을 준다. 그 이유는 Free List에서 적절한 Chunk 크기를 찾아 Allocation 해야 되기 때문이며 시간도 오래 걸린다. 즉 이말은 Young 영역에서의 체류 시간이 길어진다는 의미이다. 그렇다고 Compaction을 적용하기에도 해당 연산은 비용이 높다. 그때그때 상황에 맞게 사용하는 것이 좋을 듯 하다.

 

CMS Collector의 Floating Garbage 문제 : Old Area

CMS Collector는 Floating Garbage 문제가 있다. 만약 Initial 단계가 아니라 Concurrent Mark 단계에서 새롭게 Old 영역에 Promotion된 Object가 있다고 생각하자. 여기에서 Concurrent Mark 단계가 끝나기 전에 새로 Promotion된 Object의 참조관계가 끊어졌다. 이러면 과연 이 Object가 GC 대상이 될까? 아니다. CMS Collector는 initial mark 단계에서 마킹된 Object만 GC 대상이 되기 때문에 Concurrent Mark 단계에 Promotion되고 참조가 끊기 Object는 다음 GC 시간때 자원 해제된다. 이러한 문제를 Floating Garbage문제라고 한다. 이러한 문제를 해결하기 위해 CMS Collector는 통계정보를 계속 수집하여 적당한 GC Scheduling 시간을 잡는다. 가장 적당한 스케줄링 시간이 Remark 단계가 Minor GC 중간 지점인 스케줄링 시간이다.

 

CMS Collector 옵션

 

-XX:+UseConcMarkSweepGC

이 플래그는 애초에 CMS 컬렉터를 활성화하기 위해 필요하다. 기본적으로, HotSpot 은 처리율 컬렉터를 대신 사용한다.

-XX:+UseParNewGC

CMS 컬렉터를 사용할때, 이 플래그는 다중 쓰레드를 사용해 young generation GC의 parallel 실행을 활성화한다. 아마도 컨셉상 young generation GC 알고리즘에서 사용되는 것과 같기때문에 처리량 컬렉터에서 봤던 -XX:+UseParallelGC 플래그를 다시 사용하지 않는다는데 놀랄 것이다. 하지만, young generation GC 알고리즘과 old generation GC 알고리즘의 상호작용이 CMS 컬렉터에서 다르며 young generation 에서의 구현은 서로 달라 결국 이 둘 플래그는 다른 것이다.

주목할 것은 현재 JVM 버전에서 -XX:+UseConcMarkSweepGC를 지정하면 자동적으로 -XX:+UseParNewGC가 활성화 된다. 결과적으로, 만일 parallel young generation GC가 매력적이지 않다면 -XX:-UseParNewGC 세팅함으로써 비활성화할 필요가 있다.

-XX:+CMSConcurrentMTEnabled

이 플래그를 지정하면, 동시적 CMS 단계는 다중 쓰레드로 동작한다. 따라서 다중 GC 쓰레드는 모든 애플리케이션 쓰레들에서 parallel에서 작동한다. 이 플래그는 이미 기본값으로 활성화된다. 만약 serial 실행이 더 바람직하다면, 하드웨어 사양을 고려했을때, 다중 쓰레드 실행은 -XX:-CMSConcurrentMTEnabled 를 통해서 비활성화될 수 있다.

-XX:ConcGCThreads

-XX:ConcGCThreads=<value> 플래그는 (이전 버전에서 -XX:ParallelCMSThreads 으로 알려진) 동시적 CMS 단계가 동작할때에 사용할 쓰레드 개수를 정의한다. 예를들어, 값이 4면 CMS 주기의 모든 동시적 단계는 4개의 쓰레드를 사용해 동작한다는 뜻이다. 비록 높은 쓰레드의 개수가 동시적 CMS 단계의 속도를 높여줄수 있지만 추가적으로 동기화 오버헤드를 발생시킨다. 따라서 특정 애플리케이션을 다룰때, CMS 쓰레드의 수의 증가가 실제로 성능향상을 가지고 오는지 그렇지 않는지를 측정해야만 한다.

만일 이 플래그를 명시적으로 지정해주지 않으면 JVM은 처리량 컬렉터에서 알려진 -XX: ParallelGCThreads 플래그 값에 기반에 기본값 parallel CMS 쓰레드 수를 계산한다. 사용된 계산 공식은 ConcGCThreads = (ParallelGCThreads + 3)/4 이다. 따라서 CMS 컬렉터에서, -XX:ParallelGCThreads 플래그는 stop-to-world GC 단계에서만 영향을 주는게 아니라 동시성 단계에서도 영향을 준다.

요약을 하자면, CMS 컬렉터 실생에 다중쓰레드 설정을 위한 몇가지 방법이 있다. 이렇게 말하는 이유는, 먼저 CMS 컬렉터의 기본 세팅값을 사용해보고 튜닝이 필요하면 먼저 측정을하도록 권고하기 때문이다. 프로덕트 시스템에서(혹은 프로덕트와 유사한 테스트 시스템에서) 측정이 애플리케이션의 목표로하는 일시 정지 시간에 도달하지 않았다면 이러한 플래그를 통한 GC 튜닝은 고려해볼만 하다.

-XX:CMSInitiatingOccupancyFraction

처리량 컬렉터는 힙이 꽉 찼을때만 GC 주기를 시작한다. 예를들어, 새로운 객체를 할당할 공간이 없거나 객체를 old generation으로 승격시킬 공간이 없을때. CMS 컬렉터에서는 동시적 GC 동안에도 애플리케이션이 동작중여야하기 때문에 오랜시간을 기다리면 안된다. 따라서, 애플리케이션이 out of memory 되기전에 GC 주기를 끝내기 위해서 CMS 컬렉터는 처리량 컬렉터보다 일찍 GC 주기를 시작할 필요가 있다.

서로 다른 애플리케이션이면 객체 할당 패턴도 서로 다르며, JVM은 실제 객체 할당 및 비할당에 대한 런타임 통계를 수집하고 CMS GC 주기를 언제 시작할지 결정할때 사용한다. 이 과정을 부트스랩기, JVM은 첫번째 CMS 실행을 시작할때 힌트를 획득한다. 그 힌트는 -XX:CMSInitiatingOccupancyFraction=<value> 통해서 퍼센트로 old generation 힙 공간의 사용량으로 표시되어 지정될 수 있다. 예를들어 값이 75면 old generation의 75%를 획득했을때에 첫번째 CMS 주기를 시작하라는 의미다. 전통적으로 CMSInitiatingOccupancyFraction 의 기본값은 68 이다. (오랫동안 경험을 통해 얻은 수치다)

-XX+UseCMSInitiatingOccupancyOnly

우리는 런타임 통계에 기반해 CMS 주기를 시작할지 결정하지 않도록 JVM 에게 지시하기 위해 -XX+UseCMSInitiatingOccupancyOnly 를 사용할 수 있다. 대신, 이 플래그가 활성화된 때에, JVM은 첫음 한번만이 아닌 매번 CMS주기에서 CMSInitiatingOccupancyFraction 값을 사용한다. 그러나, 대부분의 경우 JVM이 우리 인간보다 GC 의사결정을 좀 더 잘한다는 것을 염두에 둬야 한다. 따라서, 이것은 합당한 이유가(ex, 측정을 통해 데이터를 얻었을때에) 있을때 뿐만아니라 애플리케이션에 의해서 생성된 객체의 생명주기에대해서 실제로 좋은 지식을 가지고 있는 경우에 이 플래그를 사용해야 한다.

-XX:+CMSClassUnloadingEnabled

처리량 컬렉터와 비교해, CMS 컬렉터는 기본적으로 permanent generation 에 GC를 수행하지 않는다. 만약 permanent generation GC가 필요하다면, -XX:+CMSClassUnloadingEnabled 통해서 활성화될 수 있다. 이전버전의 JVM에서는 추가적으로 -XX:+CMSPermGenSweepingEnabled 플래그 지정이 필요했었다. 주의할 것은, 이 플래그를 지정하지 않는다하더라도, 공간이 부족하게 되면 permanent generation 의 가비지 컬렉트를 시도할 것이지만 컬렉션은 동시적으로 수행되지 않을 것이다. – 대신, 다시 한번, 전체 GC가 동작할 것이다.

-XX:+CMSIncrementalMode

이 플래그는 CMS 컬렉터의 점진적 모드(incremental mode)를 활성화 한다. 점진적 모드는 애플리케이션 쓰레드에 완전히 양도(yield)되도록 동시적 단계를 주기적으로 잠시 멈추게 한다. 결론적으로, 컬렉터는 전체 CMS 주기를 완료시키기 위해서 아주 오랜시간을 가질 것이다. 따라서, 점진적 모드 사용은 일반적인 CMS 사이클에서 동작중인 컬렉터 쓰레드가 애플리케이션 쓰레드와 아주 많이 간섭이 발생되고 있다고 측정되어질경우에 유효하다. 이것은 동시적 GC 수용을 위해 충분한 프로세스를 활용하는 현대 서버 하드웨어에서 아주 드물게 발생된다.

-XX:+ExplicitGCInvokesConcurrent 와 -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses

오늘날, 가장 폭 널게 받아들여지는 최고의 기술은 애플리케이션에서 System.gc() 호출에 의해서 명시적으로 호출되는 GC를 차단하는 것이다. 이러한 조언이 사용되어지는 GC 알고리즘과 관련이 없다고 생각하고 있는데, CMS 컬렉터를 사용하고 있을때에 시스템 GC는 기본적으로 전체 GC를 발생시키는 아주 않좋은 이벤트이기 때문에 언급하고 넘어간다. 운좋게도, 기본 행동을 바꿀수 있는 방법이 있다. -XX:+ExplicitGCInvokesConcurrent 플래그는 JVM이 시스템 GC 요청이 있을때마다 전체GC 대신 CMS GC를 실행하도록 지시한다. 두번째 플래그인 -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses 는 시스템 GC가 요청될 경우에 추가로 permanent generation 을 CMSGC에 포함하도록 해준다. 따라서, 이러한 플래그의 사용으로, 우리는 예기치못한 시스템GC의 stop-to-world에 대해서 우리 스스로 보호할 수 있다.

-XX:+DisableExplicitGC

CMS 컬렉터 주제를 다루는 동안, 어떤 타입의 컬렉터를 사용하던지간에 시스템 GC 요청을 완벽하게 무시하도록 JVM에게 지시하게하는 -XX:+DisableExplicitGC 플래그를 언급할 좋은 기회다. 필자에게, 이 플래그는 더 이상 생각할 필요도 없이 모든 JVM 운영에서 안전하게 지정되어질수 있도록 기본 플래그 세트에 포함된다.

 

 

Parallel Compaction Collector

Parallel Collector의 Old Area에 새로운 알고리즘이 추가된 개념으로 multi CPU에서 유리하다. Old Area의 Collection 시간을 감소시켜 효율이 증가하나 몇몇 애플리케이션이 Large System을 공유해 CPU를 확실히 점유 못하면 오히려 제 성능을 발휘하지 못한다. 이 경우 Collection의 스레드 개수를 줄여서 사용한다.(-XX:ParallelGCThreads=n 옵션으로 조정) 다시 이야기하면 Young GC는 Parallel Collector와 동일하지만 Old 영역의 GC는 다음의 3단계를 거쳐 수행된다.

 

 

 

 

  • Mark Phase 단계 : 살아 있는 객체를 식별하여 표시해 놓는 단계
  • Summary Phase 단계 : 이전에 GC를 수행하여 컴팩션된 영역에 살아 있는 객체의 위치를 조사하는 단계
  • Compact Phase 단계 : 컴팩션을 수행하는 단계로 수행 이후에는 컴팩션된 영역과 비어 있는 영역으로 나뉜다.

Mark Phase 단계

Reachable Object를 체크하며 병렬작업으로 수행된다. Old Area의 Region(논리적 구역,2KB 정도의 Chunk)이라는 단위로 균등하게 나눈다. Region이 구분되면 Collection Thread들은 각각 Region 별로 Live Object를 Marking 한다. 이 때 Live Object의 Size, 위치 정보 등 Region 정보가 갱신된다. 이 정보들은 각 Region별 통계를 낼 때 사용한다.

 

 

Summary Phase 단계

하나의 스레드가 GC를 수행, 나머지는 애플리케이션을 수행, Summary 단계가 Mark 단계 결과를 대상으로 작업을 수행한다. Region 단위이며 GC 스레드는 Region의 통계 정보로 각 Region의 Density(밀도)를 평가한다. Density는 각 Region마다 Reachable Object의 밀도를 나타내는데 Density를 바탕으로 Dense prefix를 설정하게 된다. Dense prefix는 Reachable Object의 대부분을 차지한다고 판단되는 Region이 어디까지인가 구분하는 선이며 더불어 다음 단계의 대상을 구분해 준다. Dense prefix 왼편(메모리 번지가 작은 쪽)에 있는 Region은 이 후 GC 과정에서 제외, 나머지만 Compaction이 수행된다.

 

 

Hotspot JVM에서 Compaction은 보통 Sliding Compaction을 의미하는 데 이는 Live Object를 한쪽 방향으로 이동시킨다. 오랜 기간 참조된 Object는 더 오래 Heap에 머무를 가능성이 큰데 이를 전제로 어느 지점까지의 Object는 거의 Garbage되지 않을 확률이 높고 그게 왼쪽으로 갈 수록 높아진다. 즉 Parallel Compaction Collector는 Region별 Density를 평가하고 GC를 수행할 범위를 정하는(Dense prefix) 작업을 해 Compaction의 범위를 줄여 결과적으로 GC 소요시간을 줄인다. Dense prefix 설정 작업이 끝나면 Compaction의 대상이 되는 Region의 First Live Object 주소를 찾아 저장 작업을 수행한다.

 

Compaction Phase 단계

 

 

Heap을 잠시 Suspend 상태로 만들고 Thread들이 각 Region을 할당 받아 Compaction을 수행한다. Compaction 작업은 Garbage Object를 Sweep하고 Reachable Object를 왼편으로 몰아넣는 것이다. 작업은 Destination Region, Source Region을 각각 구별하여 작업을 수행한다. 위 그림에서 Dense prefix(3번째 Region의 시작이 Dense prefix였다.-summary 단계 그림 참조) 이후 Region 들이 Destination과 Source로 구분된다. 마지막 Region은 모두 Garbage Object 들로만 되어서 별도로 구분되지 않았다. Region마다 배정된 스레드가 Garbage Object를 Sweep 한다. Source Region 외 Region에는 Garbage Object가 사라지며 Destination Region에는 Live Object만 남게 되고 스레드-1이 Source Region의 Garbage들을 수거한다. 마지막으로 Source Region에 남겨진 Live Object는 Destination Region으로 이동한다.(Compact)

 

Parallel Compaction Collector 옵션

 

옵션 상세 설명
-XX:+UseParallelOldGC Parallel Compact Collector를 사용한다는 설정
-XX:+UseParallelOldGCCompacting

Parallel Compaction Collector 사용 시 설정한다.

Parallel Compaction 사용 여부를 설정한다.(기본값 true)

-XX:+UseParallelDensePrefixUpdate Summary phase 단계에서 Dense prefix를 매번 갱신 할지에 대한 설정이다.(기본값 true)

 

Garbage First Collector(G1GC)-(추후 정리)

CMS Collector에 비해 Pause 시간이 개선되었고 예측 가능한 게 장점이다. Young Area에 집중하면 효율이 좋으나 CMS처럼 Old Area의 단편화가 있으며 Free List 사용의 문제점과 Suspend 시간이 길어지는 현상등이 있다. G1은 물리적 Generation 구분을 없애고 전체 Heap을 1M 단위 Region으로 재편한다. Parallel Compaction Collector의 Region 단위 작업과 Incremental의 Train 알고리즘을 섞어 놓은 느낌이다. Mark 단계에서 최소한의 Suspend 시간으로 Live Object를 골라내는 것은 Parallel Compaction Collector와 비슷하다. Region 별로 순차적인 작업이 진행되고 Remember set을 이용한다. Garbage First 라는 의미는 Garbage로만 꽉 찬 Region부터 Collection을 시작한다는 의미로 발견 되자 마자 즉각 Collection한다. Garbage Object가 대부분인 Region의 경우 Live Object는 Old Region에서 다른 Old Region으로 Compaction이 이루어진다. G1 Collector에서 Young, Old Area는 물리적 개념이 아니고 Object가 Allocation되는 Region의 집합을 Young Generation, Promotion되는 Region의 집합을 Old Generation이라고 한다.

 

G1 Collector가 Region 내에 참조를 관리하는 방법은 Remember set을 이용한다. 전체 Heap의 5% 미만 공간을 각 Region의 참조정보를 저장하는 Remember set으로 할당된다. Remember set은 Region 외부에서 들어오는 참조 정보를 가지고 있다. 이로 인해 Marking 작업 시 trace 일량을 줄여줘 GC 효율을 높히게 된다.

 

 

기본적으로 Allocation 때 메모리 압박이 생기거나 임계값이 초과될 때마다 GC가 발생한다.

 

Young GC(Evacuation Pause)

GC는 Young Area부터 시작하며 Minor GC와 동일한 개념이다. Suspend Time이 존재하며 Multi Thread가 작업한다. Live Object는 Age에 맞게 Survivor Region, Old Region으로 Copy되며 기존 공간은 해지된다. 이후 새로운 Object가 할당되는 Young Region은 Survivor Region과 그 근처에 비어있는 Region이 된다.

 

Concurrent Mark phase(Mark -> Remark) : Old Area GC 시작

 

Marking 단계 : Single Thread로 수행하며 이전 단계 때 변경된 정보를 바탕으로 빠르게 Initial Mark를 진행한다.

Remarking 단계 : Suspend가 있고 전체 스레드가 동시에 작업한다. 각 Region마다 Reachable Object의 Density를 계산 한 후에 Garbage Region은 다음 단계로 안 넘어가고 바로 해지된다.

 

Concurrent Mark phase에서는 snapshot-at-the-beginning(SATB) Marking 알고리즘을 사용한다. 이는 GC를 시작할 당시 참조를 기준으로 모든 Live Object의 참조를 추적하는 방식이다. 단 Mark 작업은 Concurrent phase이므로 참조가 계속 변경된다. G1 Collector는 위 작업을 위해 write barrier를 이용, 각 참조의 생성/분리를 기록한다. 이 기록은 GC 시작 시 만들어진 snapshot과 비교되어 Marking 작업을 빠르게 수행한다.

 

Old Region reclaim phase(Remark -> Evacuation)

 

 

Remark 단계 : concurrent 작업, Multi Thread 방식으로 수행한다. GC를 위해 Live Object의 비율이 낮은 몇 개의 Region을 골라낸다.

Evacuation Pause 단계 : 독특하게 Young Area의 GC를 포함, 앞의 Remark 단계에서 골라낸 Old Region은 

 

아직 작성중 ㅠㅠ..