[JAVA] JVM - Garbage Collection

impala·2023년 1월 2일
0

JAVA

목록 보기
6/6
post-thumbnail

Garbage Collection

Garbage Collection은 Java의 메모리 관리 기법으로, Heap영역에 동적으로 할당된 메모리들 중 사용되지 않는 인스턴스를 자동으로 식별하여 해제하는 작업이다.

Garbage Collection의 장점

C/C++과는 달리 Java는 Garbage Collector가 메모리를 알아서 관리해주기 때문에 개발자가 동적으로 할당된 메모리 전체를 관리할 필요가 없다.
따라서 해제된 메모리에 접근하거나, 이미 해제된 메모리를 다시 해제하는 등의 버그나 불필요한 작업을 해소할 수 있고, Garbage Collector가 자동으로 사용되지 않는 인스턴스에 할당된 메모리를 해제하기 때문에 메모리의 누수를 막을 수 있다.

Garbage Collection의 단점

하지만 Garbage Collection이 수행되는 정확한 시점을 알 수 없고, GC작업은 순수 오버헤드이기 때문에 성능저하의 원인이 될 수 있다.
Garbage Collection이 수행될 때에는 다른 모든 쓰레드가 멈추고 GC를 수행하는 쓰레드만 동작하기 때문에(Stop-the-World) 실시간 통신이 필요한 어플리케이션의의 경우 GC를 위해 수 초간 쓰레드가 멈춘다면 장애로 이어질 수 있다. 또한 웹 어플리케이션의 경우에도 GC가 일어나는 동안 대기하는 수많은 요청으로 인해 시스템의 작동에 영향을 줄 수 있다.

따라서 시스템의 안정성을 위해서는 GC의 동작과정과 원리를 잘 이해하고, 서비스의 특징과 서비스가 돌아가는 환경에 적합한 Garbage Collector를 선택하는 것이 중요하다.


GC의 대상

Reachable/Unreachable Object

Java에서 인스턴스를 생성하면 Heap영역에 메모리 공간을 할당하고 레퍼런스를 통해 참조하는데, 이때 Heap영역의 인스턴스를 참조하는 레퍼런스가 저장되는 공간을 묶어 Root Space라고 한다.

Root Space는 구체적으로 각 쓰레드의 Stack, Native Method Stack과 Method Area의 Constant Pool로 나눌 수 있다.

  • Reference가 저장되는 위치
    • Stack의 지역변수
    • Method Area의 Constant Pool에 저장된 Static 변수
    • Native Method Stack 의 JNI 참조

JVM Heap영역의 인스턴스들은 ReachableUnreachable 두 종류로 나눌 수 있다.

  • Reachable Object : Root Space로부터 접근 가능한 인스턴스.
  • Unreachacble Object : Root Space로부터 접근할 수 없는 인스턴스(Garbage). GC의 대상

즉, Garbage Collector는 Heap영역의 인스턴스 중 Root Space로부터 참조가 불가능한 인스턴스들을 Unreachable로 분류하여 Garbage Collection을 진행한다


Reference Type

Heap영역의 인스턴스 중 Reachable/Unreachable한 인스턴스를 명시적으로 지정하기 위해서는 Reference Type을 이용한다.

  1. Strong Reference(Stronly Reachable) : 쓰레드의 Stack에서 직접적으로 인스턴스에 접근이 가능한 상태. null을 대입하지 않는 이상 GC의 대상으로 취급되지 않는다.
Object obj = new Object();
  1. Soft Reference(Softly Reachable) : SoftReference 객체를 통해서만 인스턴스에 접근할 수 있는 상태. GC 알고리즘에 의해 메모리가 부족하다고 판단되면 메모리 제거 대상이 될 수 있지만, 메모리 회수 시점은 GC 알고리즘에 따라 다르다.
SoftReference<Object> softReference = new SoftReference<>(new Object());
  1. Weak Reference(Weakly Reachable) : WeakReference 객체를 통해서만 인스턴스에 접근할 수 있는 상태. GC가 실행될 때 항상 메모리 회수 대상이 된다.
WeakReference<Object> weakReference = new WeakReference<>(new Object());
  1. Phantom Reference(Phantom Reachable) : PhantomReference 객체를 통해서만 인스턴스에 접근할 수 있는 상태. 인스턴스를 올바르게 삭제하고 삭제 이후에 작업을 조작하기 위해 사용. finalize() 메소드에 의해 컨트롤이 가능함

GC의 동작

JVM의 Garbage Collector는 GC가 필요하다고 판단되는 시점에

  1. 작업중인 쓰레드를 멈추고 (Stop The World)
  2. Garbage를 선정하여 정리 (Mark and Sweep)

의 순서로 Garbage Collection을 수행한다.

Stop The World

  • GC를 위해 JVM이 Java어플리케이션의 실행을 멈추는 작업
  • GC가 실행될 때에는 GC 쓰레드를 제외한 모든 쓰레드들의 작업이 중단되고, GC가 완료되면 작업이 재개된다
  • 어플리케이션이 멈췄다가 다시 실행되기 때문에 어플리케이션의 성능에 영향을 준다
  • 일반적으로 GC튜닝은 Stop The World상태의 시간을 줄이는 데 초점을 맞춘다

Garbage 선정 알고리즘

Garbage Collector가 Heap영역의 인스턴스를 구분하여 Garbage를 선정하는 방법은 Reference CountingMark and Sweep 두 가지 방식이 있다.
JVM의 Garbage Collector는 Mark and Sweep알고리즘을 사용한다.

Reference Counting

Heap영역의 인스턴스들이 각자 자신에게 접근 가능한 레퍼런스의 개수(Reference Counter)를 세어, Reference Counter가 0이 되면 Unreachable한 상태로 판단하고 Garbage로 분류하는 방법이다.

Reference Conting방식은 간단하면서 효율적인 알고리즘이기 때문에 python, C#, Swift등의 언어에서 널리 사용되지만 가장 큰 단점으로 순환참조를 식별할 수 없는 문제점이 있다.

위의 그림에서 각 인스턴스들은 Reference의 개수를 저장하고 있어 Reference Counter가 0인 주황색 인스턴스들은 GC에 의해 관리된다.
하지만 빨간색 테두리로 표시한 인스턴스의 경우 서로를 참조하고 있어 Reference Counter가 1로 유지되므로 GC의 대상으로 분류되지 않지만, Root Space로부터 이 인스턴스에 접근할 방법이 없어 메모리 누수가 발생한다.
따라서 Reference Counting방식은 별도의 알고리즘을 통해 순환참조 문제를 해결해야 한다는 단점이 있다.

이러한 문제점을 해결하기 위해 Java는 Mark and Sweep 알고리즘을 통해 Garbage를 선정한다.

Mark and Sweep

Root Space에서 시작하여 그래프 순회를 통해 연결된 인스턴스들을 찾아내고(Mark) Root로부터 연결되지 않은 인스턴스들을 정리하고(Sweep), 이후 선택적으로 메모리 단편화를 막기 위해 사용중인 메모리를 한 곳으로 모으는(Compact) 작업이다. (Compact과정은 필수가 아님)

Mark and Sweep알고리즘은 어떤 경우라도 Garbage를 모두 찾아낼 수 있고, 순환참조되는 인스턴스까지 정리가 가능하다는 장점이 있다.

하지만 Mark and Sweep알고리즘은 프로그램 후반부에 GC가 동작할 경우 메모리를 검색하는데 많은 비용이 소모되고, 멀티스레드 상황에서 동기화를 보장하기 어려워지기 때문에 GC가 수행될 때마다 프로그램이 멈추는 상황이 발생한다.

Mark and Sweep에서 프로그램이 멈추는 문제를 해결하기 위해 도입한 방법이 바로 앞서 설명한 Stop The World이다.

또한 Reference Counter가 0이 되면 자동으로 GC가 수행되는 Reterence Counting방식과 달리 Mark and Sweep방식은 의도적으로 GC를 실행시켜주어야 하는데, JVM의 Garbage Collector는 GC가 동작하는 시점을 Heap의 구조를 통해 효율적으로 구현하였다.


GC 발생 시점

Weak Generational Hyphothesis

  • 대부분의 객체는 금방 Unreachable상태가 된다
  • 오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다

JVM에서 GC가 발생하는 시점을 알아보기 전에, JVM의 Garbage Collector는 위의 두 가지 전제조건을 가지고 만들어졌다. 이 가설의 장점을 최대한 살리기 위해서 JVM은 Heap영역을 크게 2가지 공간(Young Generation, Old Generation)으로 나누어 효율적인 Garbage Collection을 구현하였다.


Heap의 구조와 Garbage Collection

GC가 발생하는 시점을 알아보기 위해서는 먼저 Heap의 구조를 알아야 한다.
JVM의 Heap 구조에 대해서는 JVM Heap에 정리 해 두었다.

JVM의 Heap 영역은 크게 Young Generation과 Old Generation으로 나뉘는데, GC가 발생하는 영역에 따라 Minor GC, Major GC, Full GC로 나눌 수 있다.

  • Minor GC : Young Generation의 Garbage를 처리하는 GC
  • Major GC : Old Generation의 Garbage를 처리하는 GC
  • Full GC : 모든 영역을 Garbage를 처리하는 GC

Full GC는 Heap의 모든 영역을 탐색하여 Garbage를 찾아 정리하기 때문에 시간이 많이 소요된다. 따라서 JVM은 Full GC가 발생하는 횟수를 줄이기 위해 Heap영역을 나누어 Minor GC와 Major GC를 통해 Garbage를 관리한다.


인스턴스의 생명주기

Minor GC와 Major GC가 발생하는 시점을 알아보기 위해 인스턴스가 생성되고 GC에 의해 소멸되기까지의 모든 과정을 Heap영역 내에서의 인스턴스 이동에 초점을 맞추어 설명한다.

1. 인스턴스 생성

Heap의 Young Generation영역은 다시 Eden, Servivor 0, Servivor 1영역으로 나누어진다.

  • Eden : 새롭게 생성된 객체들이 할당되는 영역
  • Servivor : Minor GC로부터 살아남은 객체들에게 할당되는 영역
    • Servivor0, 1영역중 하나는 항상 비어있어야 한다

먼저, 새로운 객체가 생성되면 가장 먼저 Heap의 Eden영역에 할당된다.
이후 새로운 객체들이 계속 생성되다가 Eden영역이 가득 차게되면 Minor GC가 발생한다.

2. Minor GC : Stop and Copy

Heap의 Eden영역이 가득 차게 되면 Eden영역에 있는 인스턴스에 대해 Mark and Sweep이 발생하고, JVM은 살아남은 객체들의 Age-bit를 1씩 증가시키고 Servivor 0영역으로 이동시킨다.

이후 다시 Eden영역이나 Servivor 0 영역이 가득 차게 되면 Eden영역과 Servivor 0영역에 있는 인스턴스에 대해 Minor GC가 수행되고, 살아남은 객체들은 Age-bit를 1씩 증가시키면서 Servivor 1영역으로 이동한다.
이 과정을 반복하다가 Age-bit가 임계값에 다다르면 Promotion이 발생한다

Minor GC에서 두 Servivor영역 사이를 반복해서 이동하는 이유는 Mark and Sweep 알고리즘이 진행되면서 메모리가 단편화되는데, 단편화된 메모리는 접근하기 불편해지므로 이를 피하기 위해 한 곳에 몰아넣어 관리하기 위한 것이다.

참고로 Minor GC 가 Stop and Copy알고리즘으로 불리는 이유는 GC가 발생할 때 Stop-the-World상태에서 Eden에서 Servivor로, 혹은 Servivor 사이에서 객체를 이동시킬 때 살아남은 객체를 복사하고 기존 영역을 모두 비우는 방식으로 동작하기 때문이다.

3. Promotion

Garbage Collector는 Servivor영역에 있는 객체 중 Age-bit가 임계값에 다다른 객체를 오래 생존할 것으로 판단하여 Old영역으로 이동시킨다. 이 작업을 Promotion이라 한다.

4. Major GC

시간이 지나 Old영역까지 가득 차게 되면 Old영역에 있는 객체에 대해 Mark and Sweep 알고리즘이 수행된다.

Card Table

Old Generation에 있는 객체가 Young Generation에 있는 객체를 참조하는 경우

Minor GC에서 Mark and Sweep은 Root Space로부터 접근이 가능한 객체를 마킹하고 마킹되지 않은 객체는 정리한다. 이때, Root Space에 포함되는 공간은 Stack, Native Method Stack, Method Area이다.

그런데 만약 (아주 적은 경우이지만) Young Generation에 있는 객체가 Root Space로부터의 연결이 끊어졌지만 Old Generation에 있는 객체가 참조하고 있는 경우라면 어떨까?

기존 방식의 경우 Heap의 Old Generation은 Root Space에 포함되지 않기 때문에 Young Generation의 객체는 Garbage로 분류될 것이다.

하지만 JVM에서는 이러한 경우를 해결하기 위해 Old Generation에 Card Table이라는 특수한 공간을 두어 Old Generation에서 Young Generation을 참조하는 객체의 정보를 담고, Minor GC가 수행될 때 Root Space뿐만 아니라 Card Table로부터 접근이 가능한 객체또한 마킹하여 Garbage로 처리되지 않게 한다.


GC의 종류

Old Generation영역에서 발생하는 Major GC는 Minor GC보다 더 많은 시간이 소요되기 때문에 구현 방식에 따라 어플리케이션의 성능이 달라진다. JDK7을 기준으로 GC는 총 5가지 방식이 있다.

Serial GC

  • 싱글 쓰레드 어플리케이션을 위한 GC
  • 하나의 쓰레드가 GC를 처리
  • 다른 GC에 비해 STW시간이 길다
  • Young Generation에서는 Mark-Sweep,
    Old Generation에서는 Mark-Sweep-Compact 알고리즘을 사용
  • 메모리와 CPU 코어 개수가 적은 환경에 적합
  • 멀티쓰레드 환경 혹은 low-latency를 추구하는 어플리케이션에서는 사용을 지양

Parallel GC(Throughput GC)

  • Java 8의 Default GC
  • 병렬처리가 가능한 GC
  • Young Generation의 GC를 멀티쓰레드로 수행(Old Generation은 단일 쓰레드로 처리)
  • Serial GC에 비해 STW시간 감소
  • 긴 pause가 허용되는 어플리케이션에 사용 가능
  • 멀티 프로세서, 멀티 쓰레드 머신에서 중간~대규모 데이터를 처리하는 어플리케이션을 위해 고안됨
  • 메모리가 충분하고 코어 개수가 많은 환경에 적합

Parallel Old GC(Parallel Compacting GC)

  • Parallel GC를 개선한 방식
  • Old Generation의 GC도 멀티쓰레드로 수행
  • Mark-Summary-Compact 알고리즘을 사용
    • sweep : 단일 쓰레드가 Old Generation 전체를 정리
    • summary : 멀티 쓰레드가 Old Generation을 분할해서 정리

Concurrent Mark & Sweep GC(CMS)

  • STW시간을 줄이기 위해 고안된 방식(Low Pause)
  • Parallel Old GC와 같이 Young Generation과 Old Generation의 GC를 병렬처리하지만, 어플리케이션 백그라운드에서 진행이 가능하다
  • Mark-Sweep 알고리즘을 Concurrent하게 수행
  • STW시간이 매우 짧아 모든 어플리케이션의 응답속도가 매우 중요할 때 사용(Low Latency GC)
  • Initial Mark : Root에서 참조하는 객체들만 식별(STW)
  • Concurrent Mark : 이전 단계에서 식별한 객체들이 참조하는 모든 객체를 추적(Concurrent)
  • ReMark : 이전 단계에서 식별한 객체들을 다시 추적하여 새로 추가되거나 참조가 끊긴 객체를 확인, Garbage를 확정(STW)
  • Concurrent Sweep : 이전 단계에서 확정된 Garbage를 정리(Concurrent)

  • 다른 GC방식보다 메모리와 CPU를 더 많이 사용한다
  • Compaction단계가 기본적으로 제공되지 않아 메모리 단편화가 발생한다
    • CPU 리소스가 부족해지거나 메모리 단편화가 심해 메모리 공간이 부족해지면 Serial GC와 동일하게 동작한다
    • Compaction이 얼마나 자주, 오랫동안 수행되는지 확인해야 함

G1 GC(Garbage First)

  • Java 9+의 Default GC
  • CMS GC를 개선한 GC
  • Heap영역을 일정한 크기의 Region으로 나누어 Region 단위로 탐색을 진행
  • Garbage가 많은 Region에 대해 우선적으로 GC를 수행
  • Mark-Sweep-Compact 알고리즘을 사용하여 메모리 단편화 문제를 해결
  • 위의 모든 GC보다 처리속도가 빠름
  • 큰 메모리공간에서 멀티 프로세스 기반으로 운영되는 어플리케이션을 위해 고안
  • 64Bit 컴퓨터에 최적화된 GC로, 4GB 이상의 Heap Size에 적합


참고 자료

0개의 댓글