가비지 컬렉션 (GC, Garbage Collection)

가비지 컬렉션(Garbage Collection)이란?

가비지 컬렉션(Garbage Collection, 이하 GC)은 자바의 메모리 관리 방법 중의 하나로 JVM의 Heap 영역에서 동적으로 할당했던 메모리 영역 중 사용하지 않는 메모리 영역을 주기적으로 삭제하는 프로세스를 말한다. C나 C++에서는 가비지 컬렉션 없이 개발자가 수동으로 메모리 할당과 해제를 일일이 해줘야 하는 반면 Java는 가비지 컬렉터가 메모리 관리를 대신 해주기 때문에 Java 프로세스가 한정된 메모리를 효율적으로 사용할 수 있게 하고, 개발자는 메모리 관리, 메모리 누수(Memory Leak) 문제에 대해 완벽하게 관리하지 않아도 되어 개발에만 집중할 수 있다는 장점이 있다. 

(가비지 컬렉션은 꼭 자바에만 있는 개념이 아니다. 자바스크립트, 파이썬 등 많은 프로그래밍 언어에 가비지 컬렉션이 기본으로 내장되어 있다.)

 

 

가비지 컬렉션 대상

그렇다면 사용하지 않는 메모리 영역은 어떤 것을 의미할까?

가비지 컬렉션은 특정 객체가 가비지인지 아닌지 판단하기 위해 도달성, 도달 능력(Reachability) 이라는 개념을 적용한다. 객체가 참조되고 있는 상태면 Reachable로 구분되고, 객체가 참조되고 있지 않은 상태면 Unreachable로 구분되어 GC의 대상이 된다.

 

예를 들어 아래와 같은 코드가 있다고 가정하자.

Car car = new Car("red"); // (1)

// garbage 발생
car = new Car("blue");     // (2)

car 변수가 (2)번 객체를 참조하게 되면서 기존의 (1)번 객체는 더이상 참조되지 않는 Unreachable 상태가 되어 가비지가 된다. 가비지 컬렉션을 수행하는 가비지 컬렉터(Garbage Collector)는 이처럼 Unreachable 상태의 객체들을 가비지로 판단하고, 이 객체들의 메모리 공간을 회수한다.

 

Mark And Sweep

Java의 초기 GC 구현은 대부분 Mark And Sweep 알고리즘 기반이었다.

https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html

  • Mark : Root Space로부터 그래프 순회를 통해 연결된 객체들을 찾아내어 각각 어떤 객체를 참조하고 있는지 찾아서 마킹한다.
  • Sweep : Unreachable한 객체들을 heap에서 제거하는 과정

 

heap 메모리 구조

JVM 힙(heap) 영역은 동적으로 레퍼런스 데이터가 저장되는 공간으로, 가비지 컬렉션 대상이 되는 공간이다.

https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html

 

Java 프로그램이 진행됨에 따라 GC에 의해 시간이 지날수록 메모리가 할당되어 있는 객체들은 적어진다.

이를 통해 대부분의 객체들은 수명이 짧다는 것을 알 수 있다. heap영역은 처음 설계될 때 다음의 2가지를 전제로 설계되었다.

  1.  대부분의 객체는 금방 접근 불가능한 상태가 된다.
  2. 오래된 객체에서 새로운 객체로의 참조는 아주 적게 존재한다.

즉, 객체는 대부분 일회성되며, 메모리에 오랫동안 남아있는 경우는 드물다는 것이다.

이러한 특성을 이용해 JVM 개발자들은 보다 효율적인 메모리 관리를 위해, 객체의 생존 기간에 따라 물리적인 heap 영역을 나누게 되었고, Young 영역과 Old 영역 총 2가지 영역으로 설계하였다. (초기에는 Perm 영역도 존재하였지만 Java 8 부터 제거됨) 


 

Yong 영역 (Yong Generation)

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

Old 영역 (Old Generation)

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

 


 

Yong 영역은 더욱 효율적인 GC를 위해 다시 3가지 영역(Eden, survivor 0, survivor 1) 으로 나누어진다.

Eden

  • new 키워드를 통해 새로 생성된 객체가 위치.
  • 정기적인 가비지 수집 후 살아남은 객체들은 Survivor 영역으로 보냄

 

Survivor 0 / Survivor 1

  • 최소 1번의 GC 이상 살아남은 객체가 존재하는 영역
  • Survivor 0 또는 Survivor 1 둘 중 하나는 꼭 비어있어야 한다.

 

더보기

Java 8 에서의 Permanent

Permanent는 생성된 객체들의 정보의 주소값이 저장된 공간이다. 클래스 로더에 의해 load 되는 Class, Method 등에 대한 Meta 정보가 저장되는 영역이고 JVM에 의해 사용된다. 

Java 7 까지는 힙 영역에 존재했지만 Java 8 버전 이후부터는 Native Method Stack에 편입되게 된다.

https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html#t3

 


Minor GC의 동작 방식

객체가 새롭게 생성되면 Young 영역 중에서도 Eden 영역에 할당된다. 그리고 Eden 영역이 꽉 차면 Minor GC가 발생하게 되는데, 사용하지 않는 메모리는 해제되고 Eden 영역에 존재하는 객체는 Survivor 영역으로 옮겨지게 된다. 

 

1. 새로 생성된 객체가 Eden 영역에 할당된다. 

2. 객체가 계속 생성되어 Eden 영역이 꽉 차게 되고 Minor GC가 실행된다.

1. 새로 생성된 객체가 Eden 영역에 할당된다.
2. 객체가 계속 생성되어 Eden 영역이 꽉 차게 되고 Minor GC가 실행된다.

 

2-1. Eden 영역에서 사용되지 않는 객체의 메모리가 해제된다.

2-2. Eden 영역에서 살아남은 객체는 1개의 Survivor 영역으로 이동된다.

2-1. Eden 영역에서 사용되지 않는 객체의 메모리가 해제된다.
2-2. Eden 영역에서 살아남은 객체는 1개의 Survivor 영역으로 이동된다.
sweep
Survivor 영역에 있는 객체들의 age 증

 

3. 1 ~ 2번의 과정이 반복되다가 Survivor 영역이 가득 차게 되면 Survivor 영역의 살아남은 객체를 다른 Survivor 영역으로 이동시킨다. (1개의 Survivor 영역은 반드시 빈 상태가 된다.)

4. 이러한 과정을 반복하여 살아남은 객체(age 값이 특정 임계치에 도달한 객체)는 Old 영역으로 이동(Promotion) 된다.

 

객체의 생존 횟수를 카운트하기 위해 Minor GC에서 객체가 살아남은 횟수를 의미하는 age를 Object Header에 기록한다. 그리고 Minor GC 때 Object Header에 기록된 age 값을 보고 Promotion 여부를 결정한다. 

또한 Survivor 영역 중 1개는 반드시 사용이 되어야 한다. 

 

더보기

Survivor Space 간의 이동마다 age 값이 증가한다. 

JVM 중 가장 일반적인 HotSpot JVM의 경우 이 age의 기본 임계값은 31이다.

객체 헤더에 age를 기록하는 부분이 6 bit로 되어 있기 때문이다.

 

Survivor 영역이 2개인 이유?

메모리가 할당되고 해제되기를 반복하다 보면 사진의 왼쪽과 같이 총 메모리 공간은 남지만, 파편화되어 있어 메모리를 할당할 수 없는 문제가 발생한다. (외부 단편화)

그래서 두 개의 Survivor 끼리 객체를 할당할 수 없을 때, 번갈아가며 메모리를 할당하며 이를 해결한다. 

만약 두 Survivor 영역에 모두 데이터가 존재하거나, 모두 사용량이 0이라면 현재 시스템이 정상적인 상황이 아님을 파악할 수 있다.

참고로 HotSpot JVM에서는 보다 빠른 메모리 할당을 위해 두 가지 기술을 사용한다.

  • bump-the-pointer
    • Eden 영역에 마지막으로 할당된 객체의 주소를 캐싱 해두는 것.
    • 객체가 할당된다면 그 객체는 Eden 영역의 맨 위에 있게 되고 그 객체가 새로운 마지막 객체로 마킹된다.
    • 마지막 객체의 위치와 크기만 알면 새로운 객체의 할당이 가능한지 알 수 있기 때문에 속도가 빠르다.
  • TLABs(Thread-Local Allocation Buffers)
    • 만약 여러 스레드에서 사용하는 객체를 Eden 영역에 저장하려면 락(lock)이 발생할 수 밖에 없고, Lock contention 때문에 성능은 매우 떨어지게 될 것이다. 이를 해결한 것이 TLABs이다.
    • 각각의 스레드마다 Eden 영역에 객체를 할당하기 위한 주소를 부여함으로써 동기화 작업 없이 빠르게 메모리를 할당하도록 하는 기술
    • 각각의 스레드는 자신이 갖는 주소에만 객체를 할당함으로써 동기화 없이 bump-the-pointer를 통해 빠르게 객체를 할당하고 있다.

 


 

Major GC의 동작 방식

Young 영역에서 오래 살아남은 객체는 Old 영역으로 Promotion 되는 것을 확인할 수 있었다. 

Major GC는 객체들이 계속 Promotion 되어 Old 영역의 메모리가 부족해지면 발생하게 된다. 

Old 영역에 할당된 메모리가 허용치를 넘게 되면, Old 영역에 있는 모든 객체들을 검사하여 참조되지 않는 객체들을 한꺼번에 삭제하는 Major GC가 실행되게 된다.

Old 영역은 Young 영역에 비해 상대적으로 크며 Young 영역을 참조할 수도 있다. 그렇기 때문에 Major GC는 일반적으로 Minor GC보다 시간이 오래걸리며, 10배 이상의 시간을 사용한다. 여기서 Stop-The-World 문제가 발생하게 된다. 

더보기

Stop-The-World

GC를 수행하기 위해 JVM이 프로그램 실행을 멈추는 현상을 의미. GC가 작동하는 동안 GC 관련 Thread를 제외한 모든 Thread는 멈추게 되어 서비스 이용에 차질이 생길 수 있다. 

 

 

 

 


 

 

[Reference]