GC(Garbage Collector)

김운채·2023년 5월 6일
0

Java

목록 보기
3/11
post-thumbnail

GC란 무엇인가?

GC(Garbage Collection)는 자바 애플리케이션에서 사용하지 않는 메모리를 자동으로 수거하는 기능을 말한다.
C/C++ 같은 언어는 메모리를 할당하고 직접 해제해야했지만, 자바에서는 GC를 이용하여 개발자들이 메모리 관리를 비교적 신경쓰지 않아도 된다.

💁‍♀️ GC장점

  • 개발자 실수로 인한 메모리 누수를 막아줌
  • 해제된 메모리 접근 막아줌
  • 해제한 메모리를 또 해제하는 이중해제 막아줌

🤦‍♀️ GC단점

  • GC 작업은 프로그램을 방해하는 오버헤드임
  • GC의 구동타이밍을 정확하게 알기 어려움

GC 이놈은 JVM 에서 heap 영역에서 활동한다. GC가 수거해가는 것들은 객체인데, 객체는 heap 영역에 저장되기 때문이다.

Reachability

근데 이 객체가 사용중인지 아닌지를 이놈이 어떻게 판단할까?🤷‍♀️
여기에서 도달성, 도달능력(Reachability) 이라는 개념을 적용한다.

객체에 레퍼런스가 있다면 Reachable로 구분되고, 객체에 유효한 레퍼런스가 없다면 Unreachable로 구분해버리고 수거해버린다.

  • Reachable : 객체가 참조되고 있는 상태
  • Unreachable : 객체가 참조되고 있지 않은 상태 (GC의 대상이 됨)

출처

JVM 메모리에서는 객체들은 실질적으로 Heap영역에서 생성되고, Method Area이나 Stack Area 에서는 Heap Area에 생성된 객체의 주소만 참조하는 형식으로 구성된다.

하지만 이렇게 생성된 Heap Area의 객체들이 메서드가 끝나는 등의 특정 이벤트들로 인하여 Heap Area 객체의 메모리 주소를 가지고 있는 참조 변수가 삭제되는 현상이 발생하면, 위의 그림에서의 빨간색 객체와 같이 Heap영역에서 어디서든 참조하고 있지 않은 객체(Unreachable)들이 발생하게 된다.

이러한 객체들을 주기적으로 GC가 제거해주는 것이다.

GC의 제거방식

그럼 어떻게 제거하는지 방식을 살펴보자.

GC를 구현하는 알고리즘 2가지가 있다.
1. Reference Counting
2. Mark and Sweep

1. Reference Counting

여기서의 Root Space 는 스택 변수, 전역변수 등 heap 영역을 참조하는 변수라고 생각하면 된다.

Reference Counting은 heap 영역에 선언된 객체들이 각각 reference count(몇가지 방법으로 해당 객체에 접근할 수 있는지를 뜻함) 라는 별도의 숫자를 가지고 있다.(초록박스)
만약 reference count가 0에 다다르면 해당 객체에 접근할 수 있는 방법이 없다는 뜻이므로 메모리 해제의 대상이 되는 것이다.

하지만 Reference Counting은 순환 참조 문제가 발생할 수 있다. 그림 속 Root Space에서 모든 Heap Space의 참조를 끊는다고 가정하자. 그러면 노란색 고리 안의 객체는 서로가 서로를 참조하고 있기 때문에 reference count가 1로 유지된다. 결국 사용하지 않는 메모리 영역이 해제되지 못하고 메모리 누수가 발생하는 것이다.

이러한 순환 참조 문제를 해결하기 위해 Mark and Sweep 이 나타난다.

2. Mark and Sweep

가비지 컬렉션이 될 대상 객체를 식별(Mark)하고 제거(Sweep)하며 객체가 제거되어 파편화된 메모리 영역을 앞에서부터 채워나가는 작업(Compaction)을 수행하게 된다.

(1) Root Space부터 해당 객체에 접근 가능한지, 아닌지를 메모리 해제의 기준으로, Root Space부터 그래프 순회를 통해 연결된 객체를 찾아낸다.(Mark)
(2) 연결이 끊어진 객체는 지운다.(Sweep)
(3) Sweep 후에 분산된 객체들을 Heap의 시작 주소로 모아 메모리가 할당된 부분과 그렇지 않은 부분으로 압축한다.(Compaction) (가비지 컬렉터 종류에 따라 하지 않는 경우도 있음)

Root Space부터 연결된 객체는 Reachable, 연결되지 않은 객체는 Unreachable라고 위에서 설명했다.

그림에서는 Sweep 이후에 분산되어 있던 던 메모리가 예쁘게 정리된 것을 볼 수 있는데, 이것은 메모리 파편화를 방지하는 Compaction 과정이다. 다만, Mark And Sweep 알고리즘에서 Compaction이 필수는 아니다.

이렇게 Mark And Sweep 방식을 사용하면, Root Space부터 연결이 끊긴 순환 참조되는 모든 객체들을 지울 수 있다. Java와 JavaScript가 Mark And Sweep 방식으로 메모리 관리를 한다.

하지만 Mark And Sweep 방식도 단점이 있다.

  • 의도적으로 GC를 실행시켜야 함 (어느 순간에는 실행 중인 애플리케이션이 GC에게 컴퓨터 리소스를 내어 주어야 한다)
  • 어플리케이션 실행과 GC 실행이 병행됨

Root Space

  • Method Area : 프로그램의 클래스 구조를 메타데이터처럼 가지며, 메서드 코드들을 저장
  • heap : 어플리케이션 실행 중에 생성되는 객체 인스턴스 저장
  • stack : 메서드 호출을 스택 프레임이라는 블록으로 쌓으며, 로컬 변수, 중간 연산 결과들이 저장되는 영역
  • pc register : 스레드가 현재 실행할 스택 프레임의 주소를 저장
  • Native Method Stack : C/C++ 등의 Low level 코드를 실행하는 스택

위에서 Root Space는 Heap 영역 메모리에 대해 참조하고 있는 영역이라고 하였는데, 구체적으로 무엇이 있는지 잠깐 살펴보자. JVM Memory 영역 중에서 Root Space는 다음 3가지가 해당된다.

✔ Stack의 로컬 변수
✔ Method Area의 Static 변수
✔ Native Method Stack의 JNI 참조

heap 메모리 구조

아까 GC의 단점이 의도적으로 GC를 실행시켜야 한다는 것이었다. 이때쯤 실행시키면 되겠당! 하는 java의 기준을 알기 위해서는 heap 영역을 좀 봐야한다.

JVM의 Heap은 크게 Young Generation과 Old Generation으로 나뉜다. 전자에서 발생하는 GC는 minor gc, 후자에서 발하는 GC는 major gc라고 부른다.

위 그림에서, Old 영역이 Young 영역보다 크게 할당되는 이유는 Young 영역의 수명이 짧은 객체들은 큰 공간을 필요로 하지 않으며 큰 객체들은 Young 영역이 아니라 바로 Old 영역에 할당되기 때문이다.

Young 영역(Young Generation), Minor GC

Young Generation은 또 다시 Eden, Survival 0, Survival 1 영역으로 나뉜다.

👉 Eden새롭게 생성된 객체들이 할당되는 영역이고,
👉 Survival 영역minor gc에서 살아남은 객체들이 존재하는 영역이다.

이때 Survival 영역에서 Survival 0과 Survival 1 중 하나는 꼭 비어 있어야 한다는 규칙이 있다.

minor gc의 실행 타이밍은 바로 Eden 영역이 꽉 찼을 때이다.

🤔프로세스를 정리해보면..

  1. 새로 생성된 객체가 Eden 영역에 할당됨
  2. 결국 Eden 영역이 꽉차게 되면 Minor GC 발동!(Mark and Sweep)
  3. Reachable 하다고 판단되는 객체는 Survival 0으로 옮겨진다. 이때 객체들의 숫자들이 0에서 1로 변하게 된다. 이는 age bit를 뜻한다. minor gc에서 살아남은 객체는 age bit가 1씩 증가하는 것이다.
  4. 다시 eden영역이 꽉차면 다시 Minor GC 발동! Reachable이라 판단된 객체들은 Survival 1 영역으로 이동한다.
  5. 다시 eden 영역이 꽉차면 Reachable 객체들이 Survival 0 으로 이동하는데 벌써 끈질기게 살아남아서 age bit가 3이 된 객체가 있다. JVM GC에서는 일정 수준의 age bit를 넘어가면 쉽게 죽지않을 놈이라 판단하고, 해당 객체를 Old Generation에 넘겨 주는데, 이를 Promotion이라 부른다.
    Java 8에서는 Parallel GC 기준 age bit 가 15가 되면 promotion이 진행된다.


Old 영역(Old Generation), Major GC

시간이 오래 지나면 여기 Old Generation도 꽉차게 될 것이다. 이때 major gc가 발생하면서 Mark And Sweep 방식을 통해 필요 없는 메모리를 비우는데, minor gc에 비해 시간이 오래 걸린다.

근데 이건 차별도아니고 왜 Heap 영역을 굳이 Young Generation과 old Generation으로 나눈걸까?

그것은 바로 GC 설계자들이 애플리케이션을 분석해 보니 대부분의 객체가 수명이 짧다는 것을 깨달았기 때문이다. GC도 결국 비용이 드는 작업인데, 메모리의 전체 부분이 아닌 특정 부분만을 탐색하여 해제해야 효율적이다. 그래서 어차피 대다수의 객체가 금방 사라지니 Young Generation 안에서 최대한 메모리를 해제하도록 설계한 것이다.

GC 실행방식

Mark And Sweep 방식의 두 번째 단점은 애플리케이션과 GC 실행이 병행된다는 것이었다.
GC가 어떤 방식으로 애플리케이션 실행과 병행되는지 살펴 보기 전에, Stop The World 개념을 알아야 한다.

Stop The World란
GC를 실행하기 위해 JVM이 애플리케이션 실행을 멈추는 것을 말한다.

Major GC가 일어나면 Thread가 멈추고 Mark and Sweep 작업을 해야 해서 CPU에 부하를 주기 때문에 멈추거나 버벅이는 현상이 일어나기 때문에, 이런현상을 개선하기 위해 우리 자바 개발자들이 어떤 노력을 해왔는지 알아boja.

1. Serial GC

서버의 CPU 코어가 1개일 때 사용하기 위해 개발된 가장 단순한 GC이다.
하나의 스레드로 GC를 실행하다 보니 Stop The World 시간이 긴 것을 알 수 있다. 싱글 스레드 환경 및 Heap 영역이 매우 작을 때 사용되는 방식이다.

2. Parallel GC

Java 8의 디폴트 GC이다.
기본적인 처리 과정은 Serial GC와 동일하다. 하지만 Parallel GC는 여러 개의 스레드로 GC를 실행하므로 앞선 Serial GC보다 Stop The World 시간이 짧아진 것을 알 수 있다. 멀티 코어 환경에서 애플리케이션 처리 속도를 향상시키기 위해 사용된다.

3. Parallel Old GC

Parallel Old GC는 Parallel GC의 업그레이드된 버전이다. major gc도 멀티 스레딩으로 수행하고 기존 Mark Sweep Compation의 개선 버전인 Mark Summary Compaction을 사용한다.

사실상 Java 7 Update 4 버전부터는 Parallel GC를 설정해도 Parallel Old GC가 동작한다. 엄밀히 말하면 Java 8의 디폴트 버전은 Parallel Old GC인 셈이다.

4. CMS GC

CMS는 Concurrent-Mark-Sweep의 줄임말로 Stop The World 시간을 최소화하기 위해 고안되었다.
어플리케이션의 쓰레드와 GC 쓰레드가 동시에 실행되어 stop-the-world 시간을 최대한 줄이기 위해 고안된 GC이지만, GC 대상을 파악하는 과정이 복잡한 여러단계로 수행되기 때문에 다른 GC 대비 CPU 사용량이 높다. 이 때문에 시스템이 장기적으로 운영되다가 조각난 메모리들이 많아 Compaction 단계를 수동으로 수행하면 오히려 Stop The World 시간이 길어지는 것을 알 수 있다.
CMS GC는 Java 9 버전부터 deprecated되었고 Java 14 버전부터는 사용이 중단되었다.

5. G1GC (Garbage First)

G1은 Garbage First의 줄임말로, Java9 부터 기본 GC로 자리잡았다.
Heap 영역을 위에서 설명한 방식과 다르게 사용한다. Heap을 일정 크기의 Region으로 잘게 나누어 어떤 영역은 Young Generation, 어떤 영역은 Old Generation으로 사용한다.

추가적으로 Humongous, Available/Unused(empty) 라는 영역이 존재한다.

  • Humongous : Region 크기의 50%를 초과하는 큰 객체를 저장하기 위한 공간. 이 Region에서는 GC 동작이 최적으로 동작하지 않는다.
  • Available/Unused(empty) : 아직 사용되지 않은 Region을 의미한다.

G1은 회수 가능한 영역, 즉 쓰레기가 많을 것으로 예상되는 영역에 대한 수집 및 압축이 주가 된다. 이것이 Garbage-First, G1으로 명명된 이유이다.

이전의 GC들처럼 일일히 메모리를 탐색해 객체들을 제거하지 않는다. 대신 메모리가 많이 차있는 영역(region)을 인식하는 기능을 통해, 메모리가 많이 차있는 영역을 우선적으로 정리한다. 즉, Heap 메모리를 전체적으로 탐색하는 것이 아니라 영역을 나눠 탐색하고 영역별로 일어난다.

G1은 힙의 하나 이상의 Region에서 단일 Region으로 개체를 복사하는 이 과정에서 메모리를 압축 및 해제 시킨다. 이 때 STW 시간을 줄이고 처리량을 늘리기 위해 다중 프로세서에서 병렬로 작동된다.

런타임에 따라 G1 GC가 필요에 따라 영역 별 Region 개수를 튜닝함으로써 Stop The World를 최소화할 수 있다.

G1GC 동작 방식

G1GC가 동작하는 프로세스는 Initial Mark -> Root Region Scan -> Concurrent Mark -> Remark -> Cleanup -> Copy 단계를 거치게 된다.

  1. Initial Mark
    Old Region 에 존재하는 객체들이 참조하는 Survivor Region 을 찾는다. 이 과정에서는 STW 현상이 발생하게 된다.
  2. Root Region Scan
    Initial Mark 단계에서 찾은 Survivor Region에 대한 GC 대상을 식별한다.
  3. Concurrent Mark
    전체 힙의 Region에 대해 스캔 작업을 진행하며, Region에 모든 객체가 Garbage라 판단되면 Remark 단계에서 즉시 제거된다. GC 대상 객체가 발견되지 않은 Region 은 이후 단계를 처리하는데 제외되도록 한다.
  4. Remark
    Stop The World 구간이다. Concurrent Mark단계에서 GC 대상을 식별하는 것을 완료하고 모든 객체가 Garbage라고 판단된 Region을 제거하고 반환한다.그리고 각 Region에대한 'liveness' 를 계산한다. Snapshot-At-The-Beginnig(SATB) 알고리즘을 사용하여 CMS GC 보다 속도를 높였다. SATB 알고리즘이란 Stop The World 이후의 살아있는 객체에만 마킹하는 알고리즘이다.
  5. Cleanup
    애플리케이션을 멈추고(STW) 살아있는 객체가 가장 적은 Region 에 대한 미사용 객체 제거 수행한다. 이후 STW를 끝내고, 앞선 GC 과정에서 완전히 비워진 Region 을 Freelist에 추가하여 재사용될 수 있게 한다.
  6. Copy
    GC 대상 Region이었지만 Cleanup 과정에서 완전히 비워지지 않은 Region의 살아남은 객체들을 새로운(Available/Unused) Region 에 복사하여 Compaction 작업을 수행한다.

참고자료 :

0개의 댓글