GC(Garbage Collector)

과녁스·2022년 2월 9일
0

Java

목록 보기
4/8
post-thumbnail

개요


자바 공부를 하면서 GC에 대한 이해가 부족하다고 느껴서 학습 및 조사

GC 개념


  • JVM(Java Virtual Machine)에서 Heap 영역에 남아있는, 더 이상 사용되지 않는 인스턴스들을 가비지라고 부른다
  • 메모리가 부족해질 경우 JVM은 이 가비지들을 삭제하여 추가로 사용할 수 있는 메모리 공간을 만든다. 이 과정을 가비지 콜렉팅 이라고 하며, 이를 수행하는 것을 가비지 콜렉터, 또는 GC 라고 한다.

동작방식


GC는 Weak generational hypothesis라는 두 가지 가설에 의해서 설계

  1. 대부분의 객체는 금방 접근 불가능(unreachable) 상태가 된다.
    • 통상적으로 어떤 메서드를 작성한다고 가정한다고 했을 때, 그 과정에서 선언되어 할당된 객체는 메서드가 종료되면, 더 이상 사용되지 않아 필요가 없어짐
    • 리턴을 하거나 매개변수에 대입하는 행위를 하지 않는다고 가정
  2. 오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다.
    • 금방 접근 불가능 상태가 되어 이후 새로 생성되는 젊은 객체를 참조하는 경우는 거의 없을 것이라고 생각

영역

객체의 생존 기간에 따라 물리접인 Heap 영역을 나누게 되었는데, 객체의 수명을 관리위한 Heap 영역을 Young, Old 구역으로 나눈다.

이미지 출처 : [Java] Garbage Collection(가비지 컬렉션)의 개념 및 동작 원리 (1/2)

Young 영역

  • 새롭게 생성된 객체가 할당(Allocation)되는 영역
  • 대부분의 객체가 금방 Unreachable 상태가 되기 때문에, 많은 객체가 Young 영역에 생성되었다가 사라진다.
  • Young 영역에 대한 가비지 컬렉션(Garbage Collection)을 Minor GC 라고 부른다.

Old 영역

  • Young 영역에서 Reachable 상태를 유지하여 살아남은 객체가 복사되는 영역
  • 복사되는 과정에서 대부분 Young 영역보다 크게 할당되며, 크기가 큰 만큼 가비지는 적게 발생한다.
  • Old 영역에 대한 가비지 컬렉션(Garbage Collection)을 Major GC 또는 Full GC라고 부른다.

예외 상황

Old 영역에 있는 객체가 Young 영역의 객체를 참조하는 경우도 존재할 것이다. 이러한 경우를 대비하여 Old 영역에는 512bytes의 덩어리(chunk)로 되어 있는 Card Table 이 존재한다.

Card Table에는 Old 영역에 있는 객체가 Young 영역의 객체를 참조할 때 마다 그에 대한 정보가 표시 된다. Card Table이 도입된 이유는 Young 영역에서 가비지 컬렉션(Minor GC)이 실행될 때 모든 Old 영역에 존재하는 객체를 검사하여 참조되지 않는 Young 영역의 객체를 식별하는 것이 비효율적 이기 때문이다. 그리하여 Young 영역에서 가비지 컬렉션이 진행될 때 Card Table만을 조회하여 GC의 대상인지 식별할 수 있도록 한다.

동작 방식

Young 영역과 Old 영역은 서로 다른 메모리 구조로 되어 있기 때문에, 세부적인 동작 방식은 다르다. 하지만 기본적으로 가비지 컬렉션이 실행된다고하면 다음 2가지 공통적인 단계를 따른다.

  1. Stop The World
  2. Mark and Sweep

Stop The World

가비지 컬렉션을 실행하기 위하여 JVM이 어플리케이션의 실행을 멈추는 작업이다. GC가 실행될 때는 GC를 실행하는 쓰레드를 제외한 모든 쓰레드들의 작업이 중단되고, GC가 완료되면 작업이 재개된다.

모든 쓰레드들이 작업이 중단되면 어플리케이션이 멈추기 때문에, GC의 성능 개선을 위해 튜닝을 한다고 하면, 보통 stop the world의 시간을 줄이는 작업을 말한다. 또한 JVM에서도 이러한 문제를 해결하기 위해 다양한 실행 옵션을 제공하고 있다.

Mark and Sweep

  • Mark : 사용되는 메모리와 사용되지 않는 메모리를 식별하는 작업
  • Sweep : Mark 단계에서 사용되지 않는 것으로 식별된 메모리를 해제하는 작업

Stop The World를 통하여 모든 작업을 중단시키면, GC는 스택의 모든 변수 또는 Reachable 객체를 스캔하면서 각각 어떤 객체를 참고하고 있는지를 탐색하게 된다. 그리고 사용되고 있는 메모리를 식별하는 과정을 Mark 라고 하낟. 이후에 Mark가 되지 않은 객체들을 메모리에서 제거하는데, 이러한 과정을 Sweep 이라고 한다.

Minor GC(Young 영역) 작동 방식

Young 영역은 다시 Eden, 두 개의 Survivor 영역으로 나뉜다.

  • Eden : 새로 생성된 객체가 할당(Allocation)되는 영역
  • Survivor : 최소 1번의 GC 이상 살아남은 객체가 존재하는 영역

Eden에는 처음 생성된 객체가 위치하게 된다. 그러다 Eden 영역이 꽉 차면 Minor GC 가 발생하게 되고, 이 영역에 위치하는 객체 중 참조되지 않는 객체는 메모리에서 제거되며, 살아남은 객체들은 Survivor 영역 두 군데 중 한 군데로 이동하게 된다.

Minor GC 가 발동할 때마다, Survivor 영역에 있던 객체들은 다른 Survivor 영역으로 이동한다. 즉 최초에 Survivor 1 영역에 있던 객체는, Minor GC가 발동하면 Survivor 2 영역으로 이동하게 된다.

  1. 새로 생성된 객체가 Eden 영역에 할당된다.
  2. 객체가 계속 생성되어 Eden 영역이 꽉차게 되고 Minor GC가 실행된다.
    1. Eden 영역에서 사용되지 않는 객체는 메모리가 해제된다.
    2. Eden 영역에서 살아남은 객체는 1개의 Survivor 영역으로 이동된다.
  3. 1~2번의 과정이 반복되다가 Survivor 영역이 가득차게 되면 Survivor 영역의 살아남은 객체를 다른 Survivor 영역으로 이동시킨다.
    • 1개의 Survivor 영역은 반드시 빈 상태가 된다.
  4. 이러한 과정을 반복하여 계속해서 살아남은 객체는 Old 영역으로 이동(Promotion) 된다.

각 객체는 Minor GC에서 살아남은 횟수를 기록하는 age bit 를 가지고 있으며, 이 age bit는 Minor GC가 발생할 때마다 하나씩 증가한다.

age bit값이 MaxTenuringThreshold 라는 설정값을 초과하게 되는 경우, Old Generation 영역으로 객체가 이동하게 된다. (JVM 옵션 : -XX:MaxTenuringThreshold 통해 설정할 수 있다. 기본값은 JVM에 의해 동적으로 정해진다.)

추가) Permanent 구역은 Heap이 아니며, Java 8 이후로는 Metaspace 영역으로 대체되었다.

이미지 출처 : [Java] Garbage Collection(가비지 컬렉션)의 개념 및 동작 원리 (1/2)

Hotspot JVM에서는 Eden 영역에 객체를 빠르게 할당(Allocation)하기 위해 bump the pointerTLABs(Thread-Local Allocation Buffers) 라는 기술을 사용하고 있다.

bump the pointer 란 Eden 영역에 마지막으로 할당된 객체의 주소를 캐싱해두는 것이다. bump the pointer를 통해 새로운 객체를 위하여 유효한 메모리를 탐색할 필요 없이 마지막 주소의 다음 주소를 사용하게 함으로써 속도를 높이고 있다. 이를 통해 새로운 객체를 할당할 때 객체의 크기가 Eden 영역에 적합한지 판별하면 되므로 빠르게 메모리를 할당 할 수 있다.

싱글 쓰레드 환경이라면 문제가 없지만, 멀티쓰레드 환경이라면 객체를 Eden 영역에 할당할 때 락(Lock)을 걸어 동기화를 해주어야 한다. 멀티 쓰레드 환경에서의 성능 문제를 해결하기 위gkdu Hotspot JVM은 추가로 TLABs(Thread-Local Allocation Buffers)라는 기술을 도입하게 되었다.

TLABs(Thread-Local Allocation Buffers)란 각각의 쓰레드마다 Eden 영역에 객체를 할당하기 위한 주소를 부여함으로써 동기화 작업 없이 빠르게 메모리를 할당하도록 하는 기술 이다. 각각의 쓰레드는 자신이 갖는 주소에만 객체를 할당함으로써 동기화 없이 bump the pointer를 통해 빠르게 객체를 할당하도록 하고 있다.

Major GC(Old 영역)

Young 영역에서 오래 살아남은 객체는 Old 영역으로 Promotion 됨을 확인할 수 있다. 그리고 Major GC는 객체들이 계속 Promotion되어 Old 영역의 메모리가 부족해지면 발생 하게 된다.

Young 영역은 일반적으로 Old 영역보다 크기가 작기 때문에 GC가 보통 0.5초에서 1초사이에 끝난다. 그렇기 때문에 Minor GC는 어플리케이션에 크게 영향을 주지 않는다. 하지만 Old 영역은 Young 영역보다 크며 Young 영역을 참조할 수도 있다. 그렇기 때문에 Major GC는 일반적으로 Minor GC 보다 시간이 오래 거릴며, 10배 이상의 시간을 사용한다.

요약

이미지 출처 : [Java] Garbage Collection(가비지 컬렉션)의 개념 및 동작 원리 (1/2)

GC알고리즘


GC 알고리즘의 기본 흐름은 GC 대상을 식별하고, 식별된 대상을 메모리에 제거하며, 필요한 경우 최적화까지 수행한다. 간단해 보이지만 이러한 알고리즘에도 여러 종류가 있으며, 각각 다음과 같다.

Reference Counting Algorithm

Garbage의 탐색에 초점을 맞춘 초기 알고리즘이다. 각 객체마다 Reference Count 라는 것을 관리하는데, 말 그대로 참조 되고 있는 갯수를 의미하며, Reference Count가 0이되면 GC를 수행한다. 단순한 구조인데다가, Reference Count가 0이 되면 즉시 메모리에서 해제된다는 장점이 있으나, Reference Count를 계속 관리해주어야 하고, Linked List 같은 순환 참조 구조에서 Memory Leak이 발생할 가능성이 크다.

Mark-Sweep-Compaction

기본적인 GC 과정으로, 다양한 GC에서 사용되는 알고리즘이다.

이름 그대로, GC 대상 객체를 식별(Mark)하고, 청소(Sweep)하고, 압축(Compaction, 앞에서부터 채움)한다.

Root Set에서 시작하는 Reference의 관계를 추적하며, Tracing Algorithm이라고도 불린다.

Mark 단계에서는 Garbage 대상 외 살아남을 객체를 마킹하며, 마킹 방식은 각 객체의 Header에 Flag를 심거나 별도의 BitmapTable을 이용한다.

Sweep 단계는 Mark 단계가 끝나면 바로 실행되며, 마킹이 없는 객체를 모두 삭제하는 작업을 한다. Sweep이 완료되면 살아남은 모든 마킹 정보를 초기화한다.

Compact 단계에서는 Sweep 단계 후 살아남은 객체들 사이사이의 빈 공간, 즉 단편화를 살아남은 객체들을 이어붙여 해결한다. 이 작업 이후 살아남은 객체들의 Reference를 업데이트하는 작업이 필요하여 부가적인 Overhead가 수반된다.

GC종류


JVM 에는 생각보다 많은 종류의 GC 알고리즘이 있다. 참고로, Java 7, 8은 기본 GC로 Parallel GC를 사용하고, Java 9, 10 은 G1 GC를 사용한다고 한다. Java 11부터는 실험적 기능인 Z GC를 사용할 수 있다. 15부터는 정식 기능으로 출시할 예정인 듯 하다.

Serial GC

순차적인 GC 라는 의미로, Mark-Sweep-Compaction 알고리즘이 한 번에 하나씩만 동작한다. 가장 오래된 GC이며, 요즘에는 사용되지 않고, 사용해서도 안된다. Stop-The-World 시간이 너무 길기 때문.

Parallel GC

Serial GC가 하나의 스레드로 Mark-Sweep-Compaction을 수행한다면, Parallel GC는 여러 개의 스레드로 Mark-Sweep-Compaction을 수행한다. 이로 인해 Stop-The-World 시간이 줄어들게 된다.

Parallel Old GC

Parallel GC와 비슷하나, Mark-Sweep-Compaction 알고리즘 대신 Mark-Summary-Compaction 알고리즘을 사용한다. Summary 작업은 Sweep 작업에 살아있는 객체를 식별하는 작업이 추가된 것이다. (이름만 봐서는 이게 더 옛날 GC같기도..)

CMS GC

앞의 GC 방식보다 더 개선되었으면서, 복잡한 방식이다. Stop-The-World 시간을 최소화 하는데 초첨을 맞췄다. 컨셉은 GC 대상 객체를 최대한 자세히 파악한 후, Stop-The-World 가 발생하는 Sweep 시간을 최소화 하는데 초점을 맞췄다. Low Latency GC라고도 부른다.
CMS GC는 총 4단계에 걸쳐 이루어진다.

  1. Initial Mark
    GC 과정에서 살아남을 객체를 Root Set에서 가장 가까운 객체만 탐색하며, 참조가 끊겼는지를 확인한다. 이 과정에서 Stop-The-World 가 일어나지만, 탐색 깊이가 짧아 멈추는 시간 역시 짧다.

  2. ConcurrentMark
    Initial Mark 단계에서 GC 대상으로 판별한 객체들을 따라가며 GC 대상인지 추가로 확인한다. 이 과정중에는 Stop-The-World 가 일어나지 않는다.

  3. Remark
    Concurrent Mark 단계의 결과를 검증한다. Concurrent Mark 단계에서 GC 대상으로 추가 확인되었는지, 참조가 제거되었는지 등 확인을 한다. 이때 Stop-The-World가 일어나며, 이 시간을 최대한 줄이기 위해 멀티스레드로 검증작업을 수행한다.

  4. Concurrent Sweep
    GC 대상으로 판별된 객체들을 멀티스레드로 메모리에서 제거한다. 이때 Stop-The-World가 발생하지 않는다.

단점으로는 하는 일이 많다보니 CPU 부하가 커진다는 것이고, Compaction이 기본적으로 제공되지 않고 필요할 때만 일어나는데, 이때의 Stop-The-World 시간이 다른 GC보다 더 길게 걸릴 수도 있다.

G1 GC (Garbage First)

하드웨어가 발전되어 JVM을 가동하는 메모리의 크기도 점점 커져가는데, 이전까지의 GC들은 큰 용량의 메모리에 적합하지 않다(Root set부터 순차적으로 탐색하기에 용량이 클 수록 탐색 시간이 길어짐).
G1 GC는 이런 점을 개선하여, 큰 Heap 메모리에서 짧은 GC 시간을 보장하는데 그 목적을 둔다.
G1 GC는 앞서 살펴본 Eden, Survivor, Old 영역이 존재하지만, 고정된 크기로 고정된 위치에 존재하지 않는다. 전체 Heap 영역을 Region이라는 특정한 크기로 나눠서, 각 Region의 상태에 따라 역할(Eden, Survivor, Old)이 동적으로 부여된다. 2048개의 Region으로 나뉠수 있으며, 옵션을 통해 1MB~32MB 사이로 지정할 수 있다.

앞에서 봤던 Eden, Survivor, Old 외에 Humonogous와 Available/Unused Region이 추가로 보인다.
Humongous는 설정된 Region 크기의 50%를 초과하는 큰 객체를 저장하기 위한 공간으로, 이 Region에서는 GC 동작이 최적으로 동작하지 않는다.
Available/Unused 는 이름에서 짐작할 수 있듯, 아직 사용되지 않은 공간이다.

Young 영역에서 GC가 수행되면 Stop-The-World 현상이 발생하며, 이 시간을 줄이기 위해 멀티 스레드로 GC를 수행한다. 동작 방식은 기존이랑 비슷한데, Eden, Survivor 영역에서 살아남은 다른 Survivor 영역으로 이동되며, 비워진 Region은 Available/Unused 상태로 돌린다.

Full GC가 수행되면 총 6개의 단계를 거쳐 이루어지게 된다.

  1. Initial Mark
    Old Region에 존재하는 객체들이 참조하는 Survivor Region을 찾는다. 이 과정에서 Stop-The-World가 발생하게 된다.

  2. Root Region Scan
    Initial Mark 에서 찾은 Survivor Region에서 GC 대상 객체를 탐색한다.

  3. Concurrent Mark
    전체 Region에 대해 스캔하여, GC 대상 객체가 존재하지 않는 Region은 이후 단계에서 제외된다.

  4. Remark
    Stop-The-World 후, GC 대상에서 제외할 객체를 식별한다.

  5. Cleanup
    Stop-The-World 후, 살아있는 객체가 가장 적은 Region에 대해서 참조되지 않는 객체를 제거한다. Stop-The-World 끝내고 완전히 비워진 Region을 Freelist에 추가하여 재사용한다.

  6. Copy
    Root Region Scan 단계에서 찾은 GC 대상 Region이었지만 Cleanup 단계에서 살아남은 객체들을 Available/Unused Region에 복사하여 Compaction 작업을 수행한다.

Z GC

비교적 최근에 나온 GC이며, 아래의 목표를 충족하기 위해 설계된 확장 가능하고 낮은 지연율(low latency)을 가진 GC이다.

  • 정지 시간이 최대 10ms를 초과하지 않음
  • Heap의 크기가 증가하더라도 정지 시간이 증가하지 않음
  • 8MB~16TB에 이르는 다양한 범위의 Heap 처리 가능

이대로라면 지금까지 나온 어떤 GC보다 혁신적에 가까운데, 어떻게 이게 가능할까?

ZGC는 ZPages라는 G1 GC의 Region과 비슷한 영역의 개념을 사용하지만, Region은 고정된 크기인 것에 반해 ZPages는 크기가 2MB의 배수로 동적으로 생성 및 삭제될 수 있다.

참고 및 출처


profile
ㅎㅅㅎ

0개의 댓글