Home GC(Garbage Collector)
Post
Cancel

GC(Garbage Collector)

GC란?


GC(Garbage Collector, 가비지 콜렉터)는 JVM 내에서 불필요한 메모리를 차지하는 객체를 자동으로 식별하여 삭제해주는 기능을 의미한다. 여기서 말하는 메모리는 힙영역을 의미한다.

객체 삭제 여부 판단 알고리즘


그러면 GC가 가지는 객체를 삭제할 것인지 계속 저장해둘 것인지를 정하는 기준은 무엇일까?

계속 사용할 객체를 Reachable 이라고 하며, 그렇지 않고 GC의 대상이 되는 객체를 Unreachable 이라고 표현한다. 그대로 번역하면 접근가능한 객체 또는 접근할 수 없는 객체 라는 뜻이다.

  • Reachable : 접근 가능한 객체
  • Unreachable : 접근 불가능한 객체

Reference Type

우선 개발자에 의해 GC의 대상이 되는 기준을 정해줄 수 있는 방법이 있다.

Strong Reference : new 예약어로 생성된 객체, null이 대입되지 않는 이상 GC의 대상이 되지 않는다.

Soft Reference : SoftReference 클래스 객체에 저장된 경우로 메모리에 여유가 있다면 GC의 대상에서 제외되지만 메모리의 공간이 없다면 GC의 대상이 된다.

Weak Reference : WeakReference 클래스 객체에 저장된 경우로 다음 GC에서 삭제 대상이 된다.

Phantom Reference : 객체의 올바른 생성과 삭제를 위해 사용되며 강한 참조로 인해 finalize() 이후에 재생성되는 경우를 방지할 수 있다.

Reference Counting

제목을 그대로 해석하면 참조 횟수를 세는 방식으로 참조 여부를 판단하는 방법이다.

Reference Counting의 장점은 알고리즘 방식 자체가 단순하다는 점이지만 참조 횟수에 대해 계속해서 체크해주기 위해 많은 오버헤드가 발생하고 순환 참조에 대해서 GC의 대상으로 식별하지 못해 삭제되지 않는 문제점을 야기하게 된다.

Mark and Sweep

Reference Counting의 단점을 보안하기 위해 나온 알고리즘이 바로 Mark and Sweep이다.

자바에서 힙 영역에 저장되는 객체들 중 Root Set이 되는 객체가 있다. 그리고 이 Root Set 객체를 기준으로 상속이나 참조하고 있는 다른 객체들이 연결되어 있는 구조로 저장된다.

Root Set이 될 수 있는 경우는 다음 세가지 경우입니다.

  • Java 스택, 즉 Java 메서드 실행 시에 사용하는 지역 변수와 파라미터들에 의한 참조
  • 네이티브 스택, 즉 JNI(Java Native Interface)에 의해 생성된 객체에 대한 참조
  • 메서드 영역의 정적 변수에 의한 참조

힙 내의 다른 객체에 의한 참조는 Root Set으로 보지 않습니다.


출처 : 링크

GC는 이러한 Root Set부터 연결된 객체들을 순회하며 Mark를 하고 Mark가 되지 않는 객체는 더 이상 Stack이나 Method 영역에서 참조하지 않는다고 판단하여 GC의 대상으로 간주(Sweep)한다.

Mark and Sweep Compact

일반 Mark and Sweep 알고리즘을 통해 GC를 동작시키면 메모리가 조각난 상태로 유지되며 이를 단편화(Fragmentation)라고 부른다. 단편화는 데이터 사용의 효율성을 떨어뜨리기 때문에 해결해야 할 문제점으로 볼 수 있다.

전체 남은 메모리양으로는 충분히 저장 가능한 객체 정보라도 조각난 메모리로는 저장이 불가능해 GC가 동작할 수 있고 이로 인해 STW가 발생해 성능의 저하까지 야기할 수 있기 때문이다.

이러한 문제를 해결하기 위해 Compact 단계를 추가한 Mark and Sweep Compact이 생겨난 것이다.

Copying Algorithm

Copying Algorithm 또한 단편화 문제를 해결하기 위한 목적으로 고안된 GC 알고리즘이며, Cheney’s Algorithm을 예로 설명하겠다.

Cheney’s Algorithm은 힙 메모리를 Active와 InActive로 나누고 두 영역의 크기를 동일하게 나누어 Active 영역에만 객체를 할당한다. 이후 Active 영역이 가득 차면 GC를 통해 메모리를 확보하는데 이 중 삭제되지 않는 객체 정보는 InActive 영역으로 복사하는 방식으로 메모리를 비운다.

이와 같이 메모리를 목적에 맞는 영역으로 나눠 관리함으로써 단편화는 해결할 수 있었다.

Generational GC

Copying Algorithm을 통해 단순하게 균일하게 메모리를 나눔으로써 실질적으로 메모리를 활용하는 공간이 줄어드는 문제점이 생겨났다. 또한 복사라는 작업이 추가되며 오버헤드도 증가하는 문제점도 야기되었다. 또한 Copying 작업을 수행할 때 Suspend(일시 중지) 현상도 발생시킨다.

메모리 활용이 왜 비효율적인지 이해하기 위해선 Weak Generational Hypothesis 개념을 이해하면 도움이 된다.

Weak Generational Hypothesis 의미는 다음과 같습니다.

  1. 생성되는 대부분의 객체는 매우 짧은 생명 주기를 가진다.
  2. 오래동안 생존한 객체의 경우, 젋은 객체로부터 참조되는 경우가 매우 드물다.

즉, 대부분의 객체의 생명주기는 짧고, 오래 살아남았다고 해서 지속적으로 사용되는 것이 아닌 사용 빈도가 매우 낮다는 것이다.

그래서 객체의 생명주기에 따라 메모리 할당을 다르게 해줘 효율적인 구조로 발전시킬 필요가 있었고 이러한 구조가 Generational GC이다.

이름대로 객체의 세대를 구분하는 방식으로 Young Generation과 Old Generation으로 구분하고 YG에서 생성되고 삭제되는 수많은 객체 중 특정 나이를 넘어선 객체에 한해서만 OG 영역으로 Promotion해 별도로 관리하는 방식을 의미한다.

이러한 Generational GC 방식을 현재 가장 널리 사용되고 있다고 볼 수 있다.

GC 메모리 영역 구조 설명



출처 : 링크

  • young : Minor GC가 일어나며 비교적 방금 생성된 객체들이 저장되는 공간으로 적은 메모리를 차지한다.
    • eden : 객체가 새로 생겨나면 저장되는 공간으로 eden영역의 메모리가 다 차면 사용하는 객체는 s0로 넘기고 그 외 객체는 삭제한다.
    • s0, s1 : eden 영역에서 넘어온 객체들이 저장되며, 동일한 원리로 메모리가 가득 차면 살아남은 객체만 s1으로 넘긴다. 다음 GC에선 edens0에서 살아남은 객체들이 s1으로 넘어가며 이 과정을 반복한다.
  • old : YG에서 특정 Age 값을 넘은 객체가 발생하는 경우 old 영역의 관리 대상이 된다. old 영역이 가득 차게 되면 Major GC가 발생하며 특정 횟수 이상을 살아남은 객체가 저장되며 발생하는 곳이고 많은 메모리를 차지한다.

Minor & Major GC

JVM의 힙영역은 설계될 때 다음과 같은 두가지 전제를 가지고 설계되었다.

  1. 대부분의 객체는 금방 접근 불가능한 상태(Unreachable)가 된다.
  2. 오래된 객체가 새로 생성된 객체를 참조하는 경우는 매우 드물다.

즉, 방금 생성된 객체가 저장되는 공간(young 영역)과 오래 살아남을 객체가 저장된 공간(old 영역)을 분리시키고 오래 살아남은 객체에 대한 불필요한 신경은 줄이며 새로 생겨나고 없어질 객체에 집중하기 위한 설계라고 볼 수 있다.

그래서 young 영역의 메모리가 가득 찼을 때, 이를 비우는 작업을 Minor GC라고 하며,
old 영역이 가득 찼을 때 발생하는 GC를 Major GC라고 한다.

GC의 작동 원리


Stop the World

말 그대로 세상이 멈춘다. 즉, GC가 작동하면 JVM 자체가 작동을 멈추게 된다.

이는 GC가 하는 작업이 객체를 삭제하는 작업이기 때문에 멀티 쓰레드가 작동하는 환경속에서 이미 삭제한 객체를 필요로 하는 쓰레드가 생겨나는 불상사를 방지하기 위한 설계라고 보여진다.(개인적인 견해)

하지만 이렇게 JVM을 중지시키는 시간이 길어지면 프로그램 성능에 매우 치명적으로 작용될 수 있다. 하지만 GC의 작업은 메모리 측면에서 필수적으로 필요하다.

그렇기 때문에 프로그램 성능의 차이가 느껴지지 않을 만큼 적은 메모리에 대해 자주 GC를 작동시켜 메모리를 관리하는 방법을 JVM은 채택하고 있으며, 이것이 Minor GC다. 그래서 young 영역은 상대적으로 적은 메모리를 차지한다. 그래야 GC가 작동하는 시간이 짧아지기 때문이다.

반대로 Major GC가 발생하는 old 영역은 많은 메모리를 차지하고 있어 한번 GC가 발생하면 긴 시간동안 JVM의 작동이 멈춰야 하는 위험이 있다. 그래서 위에서 말한 전제처럼 별로 쓰이지도 않고 오래 살아남은 객체들에게 메모리를 많이 할당하여 GC의 작동을 거의 하지 않도록 설계된 것이다.

GC 구현 방식


Serial

  • 단일 쓰레드로 GC가 작동
  • 무조건 STW 발생

Parallel

  • 병렬처리 GC로 멀티 쓰레드로 GC를 실행

CMS(Concurrent Mark Sweep) GC

  • STW를 최소화한 GC로 몇가지 단계를 통해 Reachable 객체를 구별
  • 작동 순서
    1. initial Mark : Root Set 객체만 마킹(STW 발생)
    2. Concurrent Mark : 마킹된 Root Set과 연결된 객체들을 마킹(STW 발생 X)
    3. Remark : Concurrent Mark 과정에서 마킹된 객체와 다른 객체만 추가 마킹 및 확정(STW 발생)
    4. Concurrent Sweep : 마킹되지 않은 객체 삭제(STW 발생 X)

G1 GC

하드웨어가 발전되면서 Java 애플리케이션에 사용할 수 있는 메모리의 크기도 점차 켜저갔다. 하지만 기존의 GC 알고리즘들로는 큰 메모리에서 좋은 성능(짧은 STW)을 내기 힘들었기 때문에 이에 초점을 둔 G1 GC가 등장하게 되었다.

즉, G1 GC는 큰 힙 메모리에서 짧은 GC 시간을 보장하는데 그 목적을 둔다.

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

JVM 힙은 2048개의 Region 으로 나뉠 수 있으며, 각 Region의 크기는 1MB ~ 32MB 사이로 지정할 수 있다.

G1 GC가 설정된 JVM의 힙 메모리 영역의 스냅샷은 아마도 아래와 같을 것이다.

G1 GC에서는 그동안 봐왔던 Heap 영역에서 보지 못한 Humongous, Available/Unused 이 존재하며 두 Region에 대한 역할은 아래와 같다.

Humongous : Region 크기의 50%를 초과하는 큰 객체를 저장하기 위한 공간이며, 이 Region 에서는 GC 동작이 최적으로 동작하지 않는다.

Available/Unused : 아직 사용되지 않은 Region을 의미한다.

G1 GC에서 Young GC 를 수행할 때는 STW(Stop-The-World) 현상이 발생하며, STW 시간을 최대한 줄이기 위해 멀티스레드로 GC를 수행한다. Young GC는 각 Region 중 GC 대상 객체가 가장 많은 Region(Eden 또는 Survivor 역할) 에서 수행 되며, 이 Region 에서 살아남은 객체를 다른 Region(Survivor 역할) 으로 옮긴 후, 비워진 Region을 사용가능한 Region으로 돌리는 형태 로 동작한다.

G1 GC에서 Full GC 가 수행될 때는 Initial Mark -> Root Region Scan -> Concurrent Mark -> Remark -> Cleanup -> Copy 단계를 거치게된다.

  • Initial Mark : Old Region 에 존재하는 객체들이 참조하는 Survivor Region 을 찾는다. 이 과정에서는 STW 현상이 발생하게 된ㄷ.
  • Root Region Scan : Initial Mark 에서 찾은 Survivor Region에 대한 GC 대상 객체 스캔 작업을 진행한다.
  • Concurrent Mark : 전체 힙의 Region에 대해 스캔 작업을 진행하며, GC 대상 객체가 발견되지 않은 Region 은 이후 단계를 처리하는데 제외되도록 한다.
  • Remark : 애플리케이션을 멈추고(STW) 최종적으로 GC 대상에서 제외될 객체(살아남을 객체)를 식별해낸다.
  • Cleanup : 애플리케이션을 멈추고(STW) 살아있는 객체가 가장 적은 Region 에 대한 미사용 객체 제거 수행한다. 이후 STW를 끝내고, 앞선 GC 과정에서 완전히 비워진 Region 을 Freelist에 추가하여 재사용될 수 있게 한다.
  • Copy : GC 대상 Region이었지만 Cleanup 과정에서 완전히 비워지지 않은 Region의 살아남은 객체들을 새로운(Available/Unused) Region 에 복사하여 Compaction 작업을 수행한다.

ZGC

ZGC는 자세히 설명된 블로그 링크로 대체합니다.


참고자료

This post is licensed under CC BY 4.0 by the author.