9. 궁금했던 것들 4편(1) - Garbage Collection

don9wan·2021년 11월 26일
0

반성 식탁

목록 보기
9/14
post-thumbnail
post-custom-banner

Memory Leak에 관한 이야기를 해보려 한다. 저번에 작성한 궁금했던 것들 2편 - 바인딩 클래스와 생명 주기에서 '프래그먼트에서 바인딩 클래스 인스턴스를 정리해줘야 하는 이유'에 대해서 공부했다. 문서를 보며 개발을 진행하지만, 해당 이유를 알기 전까진 인스턴스를 정리해주지 않으면 Memory Leak이 발생한다는 것을 모르고 있었다. 따라서 JVM의 GC를 맹목적으로 믿는 것이 아니라, 메모리 누수로 인한 성능 저하의 가능성은 생각보다 가까이 있다는 것을 인지하고 있는 것이 중요하다고 생각했다. 이러한 문제를 미연에 방지하기 위해선 Memory Leak이 발생할 수 있는 조건이나 상황에 대한 공부가 필요하다고 생각됐다. 일단 Memory Leak에 대해 구체적으로 알아 보자.

Memory Leak

컴퓨터 과학에서 메모리 누수(memory leak) 현상은 컴퓨터 프로그램이 필요하지 않은 메모리를 계속 점유하고 있는 현상이다. 할당된 메모리를 사용한 다음 반환하지 않는 것이 누적되면 메모리가 낭비된다. 즉, 더 이상 불필요한 메모리가 해제되지 않으면서 메모리 할당을 잘못 관리할 때 발생한다. 일부 서적에서 메모리 손실이라는 용어로 뜻을 옮기기도 하지만 leak라는 표현은 단순히 잃는 것 이상의 개념이므로 누수라는 표현이 더 정확하다.

안드로이드 앱은 JVM 상에서 실행된다. 그리고 JVM의 GC(Garbage Collector)는 불필요한 메모리를 알아서 정리해주는 역할을 수행한다. 그렇다면 GC가 작동되지 않는 상황이나 조건이 따로 있는 걸까. JVM Garbage Collector의 동작 방식에 대해 먼저 알아보고 싶어졌다.

JVM Garbage Collection

Reachability

Reachability를 직역하면 도달 가능성이다. GC는 불필요한 메모리를 정리한다. 그렇다면, 가장 첫 번째로 정의해야 할 문제는 '해당 객체가 불필요한가?'에 대한 기준을 정의하는 것이다. 해당 기준이 없다면 필요한 메모리도 정리해버리면서 대참사가 일어날 것이다. 그렇다면 이 Reachable은 어떤 기준으로 판별하는가? 바로 참조다. 어떤 객체가 유효한 참조를 가지고 있다면 도달 가능성이 있다고 판단하는 것이다.

불필요성에 대한 판별을 하기 위해, 도달 가능성이라는 기준이 정의됐다. 그렇다면 GC의 첫 번째 역할은 정해졌다. 위의 사진처럼 GC는 루트 객체(활동 상태이며 프로세스가 사용중인 객체)로부터 출발하여 해당 객체(인스턴스)가 유효한 참조가 있는지 찾아다닌다.

Generation

앞에서 GC는 Reachablity라는 기준으로 객체들을 판별하기 위해 메모리 상의 객체들을 찾아다닌다고 했다. 하지만 여기서 새로운 이슈가 발생한다. 모든 객체들을 탐색하면 탐색 시간이 길어질 수도 있지 않겠냐는 것이다. 여기서 'Generation'이라는 개념이 등장한다. 해당 개념을 알아보기 전에 객체에 대한 전제를 하나 알아보자.

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

위의 말 그대로이다. 프로세스 메모리 상에 생성되는 객체들의 수명은 대체로 짧다는 것이다. 여기서 generation이라는 기준이 등장한다. 객체들의 수명은 대체로 짧기 때문에, 항상 모든 객체에 대한 GC를 수행하기보단, 생성된지 얼마 안 지난 객체들에 대해서 집중적으로 GC를 수행하면 효율이 증가하지 않겠느냐이다. 따라서 JVMd의 Heap 영역은 처음 설계될 때부터 해당 전제를 반영하여 Heap영역을 각 Generation 구간으로 나누었다. 현재 JVM의 Heap 구간은 크게 두 Generation으로 나누어 져있다. 바로 Young Generation, Old Generation이다. 벌써부터 각 구간에 대해 감이 온다.

Young Generation

  • 새롭게 생성된 객체가 할당(Allocation)되는 영역
  • 대부분의 객체가 금방 Unreachable 상태가 되기 때문에, 많은 객체가 Young 영역에 생성되었다가 사라진다.
  • Young Generation 영역은 다시 3개의 영역으로 다시 분할된다.
    • Eden 영역: 새로 생성된 객체가 할당(Allocation)되는 영역
    • Survivor 영역: 최소 1번의 GC 이상 살아남은 객체가 존재하는 영역
  • Young-Eden 영역에 대한 가비지 컬렉션(Garbage Collection)을 Minor GC라고 부른다.

Old Generation

  • Young영역에서 Reachable 상태를 유지하여 살아남은 객체가 복사되는 영역
  • 복사되는 과정에서 대부분 Young 영역보다 크게 할당되며, 크기가 큰 만큼 가비지는 적게 발생한다.
  • Old 영역에 대한 가비지 컬렉션(Garbage Collection)을 Major GC 또는 Full GC라고 부른다.
  • Old 객체에서 Young 객체로의 참조는 아주 적게 존재하는데, 예외 상황을 대비해 Old Generation Heap 구간은 카드 테이블을 가지고 있다. 이 카드 테이블이 도입된 이유는 간단한다. Young 영역에서 가비지 컬렉션(Minor GC)가 실행될 때 모든 Old 영역에 존재하는 객체를 검사하여 참조되지 않는 Young 영역의 객체를 식별하는 것이 비효율적이기 때문이다. 그렇기 때문에 Young 영역에서 가비지 컬렉션이 진행될 때 카드 테이블만 조회하여 GC의 대상인지 식별할 수 있도록 하고 있다.

출처 - MangKyu's Diary

GC 프로세스

  • 불필요한 객체를 구분하는 기준인 Reachable
  • Generation 기준으로 분할돼있는 JVM Heap
    GC는 어떤 환경 속에서 수행되는지에 대해 간략하게 알아 봤다. 그렇다면, 실제로 GC는 어떤 프로세스를 통해 작업을 수행할까? 크게 두 단계 순서로 진행된다.

1. Stop The World

Stop The World는 가비지 컬렉션을 실행하기 위해 JVM이 애플리케이션의 실행을 멈추는 작업이다. GC가 실행될 때는 GC를 실행하는 쓰레드를 제외한 모든 쓰레드들의 작업이 중단되고, GC가 완료되면 작업이 재개된다. 당연히 모든 쓰레드들의 작업이 중단되면 애플리케이션이 멈추기 때문에, GC의 성능 개선을 위해 튜닝을 한다고 하면 보통 stop-the-world의 시간을 줄이는 작업을 하는 것이다. 또한 JVM에서도 이러한 문제를 해결하기 위해 다양한 실행 옵션을 제공하고 있다.

2. Mark and Sweep

GC가 불필요한 객체인지를 판별하기 위해 각 객체의 Reachability를 판별한다고 했다. 이는 Mark and Sweep 이라는 알고리즘에 적용된다. 무엇을 마킹하고, 무엇을 청소한다는 것일까? 당연히 reachable한 객체는 마킹하고, unreachable한 객체는 청소한다. GC의 핵심 업무는 해당 알고리즘을 통해 수행된다. 알고리즘을 간단하게 살펴 보자.

  1. Marking
    각 객체들의 Reachability를 판별하며, Reachable한 객체에 한해 마킹을 해둔다.
  2. Normal Deletion
    Unreachable한 객체들을 제거(메모리 해제)하고, 참조된 객체와 남은 공간에 대한 포인터를 유지한다.
  3. Compaction
    성능 향상을 위한 선택적 작업이다. Normal Deletion을 수행하면 Reachable한 객체들이 메모리 상에 남아 있을 것이고, 듬성듬성 존재할 수 있다. 해당 객체들을 한 곳으로 모음으로써, 메모리 환경을 깔끔하게 해준다. 해당 작업을 수행하면 이후 새로운 메모리 할당 작업을 보다 더 여유롭게 수행할 수 있다.

Minor GC

앞에서 JVM Heap - Young Generation - Eden 영역이 꽉 차게 되면 Minor GC를 수행한다고 했다. Minor GC의 프로세스는 다음과 같다.

  1. 새로 생성된 객체가 Eden 영역에 할당된다.
  2. 객체가 계속 생성되어 Eden 영역이 꽉차게 되고 Minor GC가 실행된다.
  3. Eden 영역에서 사용되지 않는 객체의 메모리가 해제된다.
  4. Eden 영역에서 살아남은 객체는 1개의 Survivor 영역으로 이동된다.
  5. 1~2번의 과정이 반복되다가 Survivor 영역이 가득 차게 되면 Survivor 영역의 살아남은 객체를 다른 Survivor 영역으로 이동시킨다.
  • 따라서 1개의 Survivor 영역은 반드시 빈 상태가 된다.
  • Survivor 영역 중 1개는 반드시 사용이 되어야 한다. 만약 두 Survivor 영역에 모두 데이터가 존재하거나, 모두 사용량이 0이라면 현재 시스템이 정상적인 상황이 아님을 파악할 수 있다.
  1. 1~3 과정을 반복하며 객체가 살아남은 횟수를 age라는 변수로 count하여 Object Header에 기록해두는데, Minor GC 수행 시 이 age를 보고 판단하여 오래 살아 남은 객체는 Old 영역으로 이동(Promotion)된다.

Major GC

시간이 지나면서 객체들이 계속 Old Generation으로 Promotion되면, Old 영역도 꽉 차는 순간이 발생한다. 이 때 Major GC가 발생하게 된다. Young 영역은 일반적으로 Old 영역보다 크키가 작기 때문에 GC가 보통 0.5초에서 1초 사이에 끝난다. 그렇기 때문에 Minor GC는 애플리케이션에 크게 영향을 주지 않는다. 하지만 Old 영역은 Young 영역보다 크며 Young 영역을 참조할 수도 있다. 그렇기 때문에 Major GC는 일반적으로 Minor GC보다 시간이 오래걸리며, 10배 이상의 시간을 사용한다.

profile
한 눈에 보기 : https://velog.io/@dongwan999/LIST
post-custom-banner

0개의 댓글