본문 바로가기

JAVA

Java - Garbage Collector

Garbage Collector(가비지 컬렉터)란?

쓰레기 수집. 메모리 관리 기법 중의 하나로, 프로그램이 동적으로 할당했던 메모리 영역중, 필요없게 된 영역의 메모리를 회수하는 기능이다. C언어와 같은 경우에는 free() 함수를 통해 직접 메모리를 해제해줘야하는데, Java나 Kotlin에서는 JVM에서 Garbage Collector가 불필요한 메모리를 알아서 정리해준다.

 

아래의 코드를 살펴보자.

String[] arr = new String[3];

arr[0] = "a";
arr[1] = "b";
arr[2] = "c";

arr = new String[] {"가비지", "컬렉터", "설명"};

처음에 선언한 arr와 아래에서 선언한 arr는 메모리 주소값이 다르다. 그렇다면 처음 선언했던 arr는 어떻게 될까? 주소를 잃어버려서 사용할 수 없는 메모리가 되어 정리가 될 것이다. 이렇게 불필요한 메모리들을 정리해주는 이유는, 메모리 누수를 방지하고, 한정된 자원을 효율적으로 사용할 수 있게 해주기 위함이다. 개발자는 메모리를 관리해주어야 하는 부담감이 줄어들게 된다.

 

GC에 대해서 알기 위해서는 JVM메모리 구조에 대해서 알고 있어야 한다. 아래의 링크를 참고하여 확인해보자

자료출처: https://mirinae312.github.io/develop/2018/06/04/jvm_gc.html

 

- JVM 메모리구조 정리내용

 

Java - JVM(Java Virtual Machine)의 메모리 영역 - 간단 정리

= 힙 영역(Heap Area) : JVM이 시작될 때 생성되어 애플리케이션이 동작하는 동안(=런타임) 동적으로 할당하여 사용되는 메모리 영역. - 힙 영역에 생성된 인스턴스와 배열은, 스택 영역의 변수나 다

xggames.tistory.com

 

Garbage Collection은 새로운 Object의 할당을 위해 한정된 Heap Area 영역을 재활용 하려는 목적으로 수행된다. 그리고 약한 세대 가설(weak generational hypothesis)을 전제로 가비지컬렉터를 동작시킨다.

- 대부분의 객체는 금방 접근 불가능한 상태(unreachable)가 된다.

- 오래된 객체에서 새로운 객체로의 참조는 아주 적게 존재한다.

이 전제조건 하에 Garbage를 처리하기 위해 2가지 영역(Young Generation, Old Generation)으로 나누어서 설계하였다. (원래는 3가지 영역이였으나, Java8부터 Permanent 영역은 제거됨)

 

Young Generation, Old Generation

= Young Generation

  • 새롭게 생성된 객체가 할상되는 영역
  • 대부분의 객체가 금방 unreachable 상태가 되기 때문에, 많은 객체가 Young Generation에 생성되었다가 사라진다
  • Young Generation에 대한 Garbage Collection을 Minor GC라 한다

= Old Generation

  • Young Generation에서 reachable 상태를 유지하여 살아남은 객체가 복사되는 영역
  • 복사되는 과정에서 대부분 Young Generation보다 크게 할당되며, 크기가 큰 만큼 Garbage는 적게 발생
  • Old Generation에 대한 GC을 Major GC 혹은 Full GC라고 한다

Java의 GC는 쓰레기 객체를 판별하기 위해 reachability라는 개념을 사용한다. 어떤 객체에 유효한 참조가 있으면 'reachable', 없으면 'unreachable'로 구분한다.

- 더 자세히 알아보기: https://d2.naver.com/helloworld/329631

 

GC의 기본 동작방식

Young과 Old Generation은 서로 다른 메모리 구조로 이루어져 있어서, 세부적이 동작방식은 다르지만, 기본적으로 GC가 실행될 때 아래의 2가지 공통적인 단계를 수행한다.

1. Stop The World (이하 STW)

: GC를 실행하기 위해 JVM이 애플리케이션의 실행을 멈추는 작업이다. GC가 실행될 때는 GC를 실행하는 쓰레드를 제외한 모든 쓰레드들의 작업이 중단되고, GC가 완료되면 작업이 재개된다. 모든 쓰레드들의 작업이 중단되면 애플리케이션이 멈추기 때문에, GC의 튜닝을 한다는 것은 이 중단되는 시간을 줄이는 작업을 하는 것이다. JVM에서도 이 문제를 해결하기 위해 다양한 실행 옵션을 제공하고 있다. (아래에서 옵션관련 설명 진행)

2. Mark and Sweep

: Mark(사용되는 메모리와 사용되지 않는 메모리를 식별하는 작업), Sweep(Mark 단계에서 사용되지 않음으로 식별된 메모리를 해제하는 작업) Stop The World 동작시 GC는 모든 객체를 스캔하여 사용되고 있는 메모리를 식별(Mark)하고 미사용 객체들을 제거(Sweep)한다.

 

GC 동작순서

 1. 처음 생성된 객체는 Young Generation의 일부인 Eden 영역에 위치하게 된다. 그리고 Minor GC가 발생하여 사용하지 않는 객체를 정리한다.

 2. Eden 영역에서 살아남은 객체는 Survivor 영역으로 이동하고, Survivor1 - Survivor2 로 서로 이동하면서 Minor GC가 발생하여 사용하지 않는 객체를 정리한다.

 3. 두 영역을 오가며 살아남은 객체들은 최종적으로 Old Generation으로 이동하여, 미사용된다고 식별되는 객체들은 Full GC를 통해 메모리에서 제거시킨다.

 

Major GC(Full GC)는 Young Generation에서 살아남은 객체가 Old Generation으로 넘어오는데, 객체들이 쌓여 Old Generation의 메모리가 부족해지면 발생한다. Old Generation은 크기때문에 일반적으로는 MinorGC보다 몇배 이상의 시간을 사용한다.

 

Minor GCEden 영역이 꽉 차면 일어나고, 살아남은 객체는 Survivor영역으로 옮겨지게 된다. Survivor 영역은 2개이지만, 반드시 1개의 영역에만 데이터가 존재해야 한다. 그리고 Survivor1과 2사이를 이동하면서 각각의 객체의 age를 Object Header에 기록하고 사용하지 않는 객체는 정리한다. age에 도달했거나 Survivor의 메모리가 꽉차면 Old Generation으로 이동시킨다.

 

(!) Young Generation 에서 살아남은 객체에 대한 기준이란?

각각의 객체는 횟수를 기록하는 age bit를 가지고 있으며, Minor GC가 동작할때 마다 1씩 값이 증가한다. 이 값이 MaxTenuringThreshold라는 설정값을 초과하게 되는 경우 Old Generation 으로 객체가 이동된다. 또는 MaxTenuringThreshold값이 초과하기 전에 Survivor 영역의 메모리가 부족한 경우에도 미리 Old Generation으로 이동할 수 있다. (이는 JVM 옵션의 '-XX:MaxTenuringThreshold=횟수' 로 설정이 가능하다)

 

GC의 여러가지 종류

- Serial GC

Serial GC란, 순차적인 GC라는 뜻이다. Serial GC의 Young Generation은 Mark-Sweep 방식으로 수행된다. 하지만 Old Generation에서는 Mark-Sweep-Compact라는 알고리즘이 사용되는데 기존 Mark and Sweep에서 Compact라는 작업이 추가되었다.

(!) Compact: 파편화된 메모리 영역을 앞에서부터 채워나가는 작업 (Window 디스크 조각모음과 같은 느낌)

=> Serial GC는 서버의 CPU 코어가 1개일 때 사용하기 위해 개발되었으며, 모든 GC 일을 처리하기 위해 1개의 쓰레드만을 이용한다. 따라서 코어가 여러개인 서버에서는 사용하면 안된다.

- Parallel GC

기본적인 처리과정은 Serial GC와 동일하지만 여러개의 쓰레드를 통해 GC를 수행함으로써 프로세스를 더 빠르게 동작할 수 있게 한다. STW 시간을 줄여줄 수 있다.

Java8 까지 채택되었던 기본 GC 방식이다.

Serial GC와 Parallel GC의 비교

- Parallel Old GC

Parallel GC를 조금 더 업그레이드 한 GC이다. 이름에 Old가 추가되었는데, Old Generation쪽 GC 알고리즘이 개선되 버전이라고 한다.

Old Generation이 Mark-Sweep-Compaction에서 Mark-Summary-Compaction 으로 변경되었다.

(!) Summary

: Region 단위로 작업을 수행한다. Region 통계 정보를 바탕으로 각 Region의 reachable 객체의 밀도를 평가 후 GC 수행 범위를 결정한다. 이를 통해 Compaction 범위를 줄여 GC의 소요시간을 줄여준다.

 

- CMS(Concurrent Mark Sweep) GC

Parallel GC와 마찬가지로 여러 개의 쓰레드를 사용하며, Mark Sweep을 Concurrent(동시다발적으로)하게 수행한다. CMS GC는 애플리케이션의 지연 시간을 최소화하기 위해 고안되었으며, 애플리케이션이 구동중일 때 GC가 프로세서의 자원을 공유하여 이용하는데 이러한 이유로 응답이 느려질 수는 있지만 응답이 멈추지 않게 된다.

- 다른 GC방식보다 메모리와 CPU를 더 많이 필요로 하다.

- Compaction(디스크 조각모음) 단계가 기본적으로 동작하지 않고 메모리 단편화가 심한 경우에만 실행한다. 이경우 STW시간이 더 길어질 수 있다

- Java 9에서부터는 deprecated되었고, Java 14에서는 사용이 중지되었다.

- Initial Mark -> Concurrent Mark -> Remark -> Concurrent Sweep

1] Initial Mark

: GC 과정에서 살아남은 객체를 탐색하는 시작 객체에서 참조 Tree상 가까운 객체만 1차적으로 찾아가며 객체가 GC대상인지를 판단한다. 이때 STW가 발생하게되지만, 탐색 깊이가 얕아 발생시간은 매우 짧다.

2] Concurrent Mark

: STW 현상없이 진행되며, Initial Mark 단계에서 GC 대상으로 판별된 객체들이 참조하는 다른 객체들을 따라가면 GC 대상인지를 추가적으로 확인한다.

3] Remark

: Concurrent Mark 단계의 결과를 검증한다. 이 검증 과정은 STW가 발생하기에, 멀티스레드로 검증 작업을 수행한다.

4] Concurrent Sweep

: STW 없이 Remark 단계에서 검증 완료된 GC 객체들을 메모리에서 제거한다.

 

- G1GC (Garbage First Garbage Collector)

장기적으로 많은 문제를 일으킬 수 있는 CMS GC를 대체하기 위해 만들어졌다. 흐름상 큰 힙 메모리에서 짧은 GC를 위해 등장하였다. G1GC는 Serial GC, Parallel GC, Parallel Old GC, CMS GC와는 다른 방식으로 Heap 메모리를 관리한다. 전체 Heap 메모리를 Region이라는 특정한 크기로 나누어서 각 Region의 상태에 따라 그 Region에 역할이 동적으로 부여된다. 메모리는 2048개의 Region으로 나뉠 수 있으며, 각 Region은 1MB ~ 32MB 사이로 지정될 수 있다.

기존 GC방식처럼 Eden, Survivor, Old 영역의 개념을 사용하며 추가로 Humonogous와 Anailable/Unused의 개념이 추가되었다.

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

    - Available/Unused: 아직 사용되지 않은 Region

 

G1GC의 핵심은 Heap을 동일한 크기의 Region으로 나누고, 가비지가 많은 Region에 대해서 우선적으로 GC처리를 수행한다는 것이다. 그리고 다른 GC처럼 MinorGC 및 MajorGC를 수행한다. 한 Region에 객체를 할당하다가 해당 지역이 꽉 차면 다른 지역에 객체를 할당하고 G1GC에서의 MinorGC는 G1GC는 각 지역을 추적하고 있기 때문에, Garbage가 가장 많은 지역을 찾아서 Mark - Sweep을 한다. Eden 지역에서 GC가 수행되면 살아남은 객체를 식별(Mark)하고, 메모리를 회수(Sweep)한다. 그리고 살아남은 객체를 다른 지역(Region)으로 이동시킨다. 그 지역이 Available/Unused이면 이 지역은 Survivor영역이 되고, Eden영역은 Available/Unused영역이 된다.

 

G1GC의 Major GC(=Full GC)는 시스템이 계속 운영되다가 객체가 너무 많아 빠르게 메모리를 회수할 수 없을 때 실행된다. G1GC는 어느 영역에 가비지가 많은지를 알고 있기때문에 GC를 수행할 지역을 조합하여 해당 지역에 대해서만 GC를 수행한다. 이러한 작업은 Concurrent(동시성)으로 수행하기 때문에 Application 지연이 최소화가 된다.

 

G1GC는 다른 GC에 비해 호출이 많지만, 적은 규모의 메모리 정리 작업이고 동시성을 가진상태로 수행하기 때문에 지연시간이 매우 짧으며, Garbage가 많이 쌓인 지역부터 정리하기 때문에 효율적인 동작 방식이다.

 

- Java 7부터 지원한다. 그리고 Java 9부터는 기본 GC로 사용되게 되었다.

참고자료

- https://velog.io/@litien/%EA%B0%80%EB%B9%84%EC%A7%80-%EC%BB%AC%EB%A0%89%ED%84%B0GC

 

- https://bkim.tistory.com/16

 

- https://blog.ddoong2.com/2019/07/29/IntelliJ-IDEA-%EC%98%B5%EC%85%98/#

 

- https://mirinae312.github.io/develop/2018/06/04/jvm_gc.html

 

- https://mangkyu.tistory.com/118

'JAVA' 카테고리의 다른 글

Java - 빌더 패턴(Builder Pattern)  (0) 2022.01.13
Java - 설정 옵션 (Garbage Collector)  (0) 2021.11.01
Java - 데이터 타입(기본타입, 참조타입)  (0) 2021.10.20
Java - Jackson  (0) 2021.09.30
Java - Enum  (0) 2021.09.15